diff options
-rw-r--r-- | src/libsystemd-terminal/term-internal.h | 82 | ||||
-rw-r--r-- | src/libsystemd-terminal/term-page.c | 954 |
2 files changed, 1036 insertions, 0 deletions
diff --git a/src/libsystemd-terminal/term-internal.h b/src/libsystemd-terminal/term-internal.h index 56ebd30cb4..d7d2f98b35 100644 --- a/src/libsystemd-terminal/term-internal.h +++ b/src/libsystemd-terminal/term-internal.h @@ -34,6 +34,9 @@ typedef struct term_attr term_attr; typedef struct term_cell term_cell; typedef struct term_line term_line; +typedef struct term_page term_page; +typedef struct term_history term_history; + /* * Miscellaneous * Sundry things and external helpers. @@ -253,3 +256,82 @@ void term_line_unlink(term_line *line, term_line **first, term_line **last); #define TERM_LINE_LINK(_line, _head) term_line_link((_line), &(_head)->lines_first, &(_head)->lines_last) #define TERM_LINE_LINK_TAIL(_line, _head) term_line_link_tail((_line), &(_head)->lines_first, &(_head)->lines_last) #define TERM_LINE_UNLINK(_line, _head) term_line_unlink((_line), &(_head)->lines_first, &(_head)->lines_last) + +/* + * Pages + * A page represents the 2D table containing all cells of a terminal. It stores + * lines as an array of pointers so scrolling becomes a simple line-shuffle + * operation. + * Scrolling is always targeted only at the scroll-region defined via scroll_idx + * and scroll_num. The fill-state keeps track of the number of touched lines in + * the scroll-region. @width and @height describe the visible region of the page + * and are guaranteed to be allocated at all times. + */ + +struct term_page { + term_age_t age; /* page age */ + + term_line **lines; /* array of line-pointers */ + term_line **line_cache; /* cache for temporary operations */ + unsigned int n_lines; /* # of allocated lines */ + + unsigned int width; /* width of visible area */ + unsigned int height; /* height of visible area */ + unsigned int scroll_idx; /* scrolling-region start index */ + unsigned int scroll_num; /* scrolling-region length in lines */ + unsigned int scroll_fill; /* # of valid scroll-lines */ +}; + +int term_page_new(term_page **out); +term_page *term_page_free(term_page *page); + +#define _term_page_free_ _cleanup_(term_page_freep) +DEFINE_TRIVIAL_CLEANUP_FUNC(term_page*, term_page_free); + +term_cell *term_page_get_cell(term_page *page, unsigned int x, unsigned int y); + +int term_page_reserve(term_page *page, unsigned int cols, unsigned int rows, const term_attr *attr, term_age_t age); +void term_page_resize(term_page *page, unsigned int cols, unsigned int rows, const term_attr *attr, term_age_t age, term_history *history); +void term_page_write(term_page *page, unsigned int pos_x, unsigned int pos_y, term_char_t ch, unsigned int cwidth, const term_attr *attr, term_age_t age, bool insert_mode); +void term_page_insert_cells(term_page *page, unsigned int from_x, unsigned int from_y, unsigned int num, const term_attr *attr, term_age_t age); +void term_page_delete_cells(term_page *page, unsigned int from_x, unsigned int from_y, unsigned int num, const term_attr *attr, term_age_t age); +void term_page_append_combchar(term_page *page, unsigned int pos_x, unsigned int pos_y, uint32_t ucs4, term_age_t age); +void term_page_erase(term_page *page, unsigned int from_x, unsigned int from_y, unsigned int to_x, unsigned int to_y, const term_attr *attr, term_age_t age, bool keep_protected); +void term_page_reset(term_page *page, const term_attr *attr, term_age_t age); + +void term_page_set_scroll_region(term_page *page, unsigned int idx, unsigned int num); +void term_page_scroll_up(term_page *page, unsigned int num, const term_attr *attr, term_age_t age, term_history *history); +void term_page_scroll_down(term_page *page, unsigned int num, const term_attr *attr, term_age_t age, term_history *history); +void term_page_insert_lines(term_page *page, unsigned int pos_y, unsigned int num, const term_attr *attr, term_age_t age); +void term_page_delete_lines(term_page *page, unsigned int pos_y, unsigned int num, const term_attr *attr, term_age_t age); + +/* + * Histories + * Scroll-back buffers use term_history objects to store scroll-back lines. A + * page is independent of the history used. All page operations that modify a + * history take it as separate argument. You're free to pass NULL at all times + * if no history should be used. + * Lines are stored in a linked list as no complex operations are ever done on + * history lines, besides pushing/poping. Note that history lines do not have a + * guaranteed minimum length. Any kind of line might be stored there. Missing + * cells should be cleared to the background color. + */ + +struct term_history { + term_line *lines_first; + term_line *lines_last; + unsigned int n_lines; + unsigned int max_lines; +}; + +int term_history_new(term_history **out); +term_history *term_history_free(term_history *history); + +#define _term_history_free_ _cleanup_(term_history_freep) +DEFINE_TRIVIAL_CLEANUP_FUNC(term_history*, term_history_free); + +void term_history_clear(term_history *history); +void term_history_trim(term_history *history, unsigned int max); +void term_history_push(term_history *history, term_line *line); +term_line *term_history_pop(term_history *history, unsigned int reserve_width, const term_attr *attr, term_age_t age); +unsigned int term_history_peek(term_history *history, unsigned int max, unsigned int reserve_width, const term_attr *attr, term_age_t age); diff --git a/src/libsystemd-terminal/term-page.c b/src/libsystemd-terminal/term-page.c index bfff3b1719..7ae90e2cda 100644 --- a/src/libsystemd-terminal/term-page.c +++ b/src/libsystemd-terminal/term-page.c @@ -62,6 +62,14 @@ * allocations on others. * Anyhow, until we have proper benchmarks, we will keep the current code. It * seems to compete very well with other solutions so far. + * + * The page-layer is a one-dimensional array of lines. Considering that each + * line is a one-dimensional array of cells, the page layer provides the + * two-dimensional cell-page required for terminals. The page itself only + * operates on lines. All cell-related operations are forwarded to the correct + * line. + * A page does not contain any cursor tracking. It only provides the raw + * operations to shuffle lines and modify the page. */ #include <stdbool.h> @@ -1140,3 +1148,949 @@ void term_line_unlink(term_line *line, term_line **first, term_line **last) { line->lines_prev = NULL; line->lines_next = NULL; } + +/** + * term_page_new() - Allocate new page + * @out: storage for pointer to new page + * + * Allocate a new page. The initial dimensions are 0/0. + * + * Returns: 0 on success, negative error code on failure. + */ +int term_page_new(term_page **out) { + _term_page_free_ term_page *page = NULL; + + assert_return(out, -EINVAL); + + page = new0(term_page, 1); + if (!page) + return -ENOMEM; + + *out = page; + page = NULL; + return 0; +} + +/** + * term_page_free() - Free page + * @page: page to free or NULL + * + * Free a previously allocated page and all associated data. If @page is NULL, + * this is a no-op. + * + * Returns: NULL + */ +term_page *term_page_free(term_page *page) { + unsigned int i; + + if (!page) + return NULL; + + for (i = 0; i < page->n_lines; ++i) + term_line_free(page->lines[i]); + + free(page->line_cache); + free(page->lines); + free(page); + + return NULL; +} + +/** + * term_page_get_cell() - Return pointer to requested cell + * @page: page to operate on + * @x: x-position of cell + * @y: y-position of cell + * + * This returns a pointer to the cell at position @x/@y. You're free to modify + * this cell as much as you like. However, once you call any other function on + * the page, you must drop the pointer to the cell. + * + * Returns: Pointer to the cell or NULL if out of the visible area. + */ +term_cell *term_page_get_cell(term_page *page, unsigned int x, unsigned int y) { + assert_return(page, NULL); + + if (x >= page->width) + return NULL; + if (y >= page->height) + return NULL; + + return &page->lines[y]->cells[x]; +} + +/** + * page_scroll_up() - Scroll up + * @page: page to operate on + * @new_width: width to use for any new line moved into the visible area + * @num: number of lines to scroll up + * @attr: attributes to set on new lines + * @age: age to use for all modifications + * @history: history to use for old lines or NULL + * + * This scrolls the scroll-region by @num lines. New lines are cleared and reset + * with the given attributes. Old lines are moved into the history if non-NULL. + * If a new line is allocated, moved from the history buffer or moved from + * outside the visible region into the visible region, this call makes sure it + * has at least @width cells allocated. If a possible memory-allocation fails, + * the previous line is reused. This has the side effect, that it will not be + * linked into the history buffer. + * + * If the scroll-region is empty, this is a no-op. + */ +static void page_scroll_up(term_page *page, unsigned int new_width, unsigned int num, const term_attr *attr, term_age_t age, term_history *history) { + term_line *line, **cache; + unsigned int i; + int r; + + assert(page); + + if (num > page->scroll_num) + num = page->scroll_num; + if (num < 1) + return; + + /* Better safe than sorry: avoid under-allocating lines, even when + * resizing. */ + new_width = MAX(new_width, page->width); + + cache = page->line_cache; + + /* Try moving lines into history and allocate new lines for each moved + * line. In case allocation fails, or if we have no history, reuse the + * line. + * We keep the lines in the line-cache so we can safely move the + * remaining lines around. */ + for (i = 0; i < num; ++i) { + line = page->lines[page->scroll_idx + i]; + + r = -EAGAIN; + if (history) { + r = term_line_new(&cache[i]); + if (r >= 0) { + r = term_line_reserve(cache[i], + new_width, + attr, + age, + 0); + if (r < 0) + term_line_free(cache[i]); + else + term_line_set_width(cache[i], page->width); + } + } + + if (r >= 0) { + term_history_push(history, line); + } else { + cache[i] = line; + term_line_reset(line, attr, age); + } + } + + if (num < page->scroll_num) { + memmove(page->lines + page->scroll_idx, + page->lines + page->scroll_idx + num, + sizeof(*page->lines) * (page->scroll_num - num)); + + /* update age of moved lines */ + for (i = 0; i < page->scroll_num - num; ++i) + page->lines[page->scroll_idx + i]->age = age; + } + + /* copy remaining lines from cache; age is already updated */ + memcpy(page->lines + page->scroll_idx + page->scroll_num - num, + cache, + sizeof(*cache) * num); + + /* update fill */ + page->scroll_fill -= MIN(page->scroll_fill, num); +} + +/** + * page_scroll_down() - Scroll down + * @page: page to operate on + * @new_width: width to use for any new line moved into the visible area + * @num: number of lines to scroll down + * @attr: attributes to set on new lines + * @age: age to use for all modifications + * @history: history to use for new lines or NULL + * + * This scrolls the scroll-region by @num lines. New lines are retrieved from + * the history or cleared if the history is empty or NULL. + * + * Usually, scroll-down implies that new lines are cleared. Therefore, you're + * highly encouraged to set @history to NULL. However, if you resize a terminal, + * you might want to include history-lines in the new area. In that case, you + * should set @history to non-NULL. + * + * If a new line is allocated, moved from the history buffer or moved from + * outside the visible region into the visible region, this call makes sure it + * has at least @width cells allocated. If a possible memory-allocation fails, + * the previous line is reused. This will have the side-effect that lines from + * the history will not get visible on-screen but kept in history. + * + * If the scroll-region is empty, this is a no-op. + */ +static void page_scroll_down(term_page *page, unsigned int new_width, unsigned int num, const term_attr *attr, term_age_t age, term_history *history) { + term_line *line, **cache, *t; + unsigned int i, last_idx; + + assert(page); + + if (num > page->scroll_num) + num = page->scroll_num; + if (num < 1) + return; + + /* Better safe than sorry: avoid under-allocating lines, even when + * resizing. */ + new_width = MAX(new_width, page->width); + + cache = page->line_cache; + last_idx = page->scroll_idx + page->scroll_num - 1; + + /* Try pulling out lines from history; if history is empty or if no + * history is given, we reuse the to-be-removed lines. Otherwise, those + * lines are released. */ + for (i = 0; i < num; ++i) { + line = page->lines[last_idx - i]; + + t = NULL; + if (history) + t = term_history_pop(history, new_width, attr, age); + + if (t) { + cache[num - 1 - i] = t; + term_line_free(line); + } else { + cache[num - 1 - i] = line; + term_line_reset(line, attr, age); + } + } + + if (num < page->scroll_num) { + memmove(page->lines + page->scroll_idx + num, + page->lines + page->scroll_idx, + sizeof(*page->lines) * (page->scroll_num - num)); + + /* update age of moved lines */ + for (i = 0; i < page->scroll_num - num; ++i) + page->lines[page->scroll_idx + num + i]->age = age; + } + + /* copy remaining lines from cache; age is already updated */ + memcpy(page->lines + page->scroll_idx, + cache, + sizeof(*cache) * num); + + /* update fill; but only if there's already content in it */ + if (page->scroll_fill > 0) + page->scroll_fill = MIN(page->scroll_num, + page->scroll_fill + num); +} + +/** + * page_reserve() - Reserve page area + * @page: page to modify + * @cols: required columns (width) + * @rows: required rows (height) + * @attr: attributes for newly allocated cells + * @age: age to set on any modified cells + * + * This allocates the required amount of lines and cells to guarantee that the + * page has at least the demanded dimensions of @cols x @rows. Note that this + * never shrinks the page-memory. We keep cells allocated for performance + * reasons. + * + * Additionally to allocating lines, this also clears any newly added cells so + * you can safely change the size afterwards without clearing new cells. + * + * Note that you must be careful what operations you call on the page between + * page_reserve() and updating page->width/height. Any newly allocated line (or + * shifted line) might not meet your new width/height expectations. + * + * Returns: 0 on success, negative error code on failure. + */ +int term_page_reserve(term_page *page, unsigned int cols, unsigned int rows, const term_attr *attr, term_age_t age) { + _term_line_free_ term_line *line = NULL; + unsigned int i, min_lines; + term_line **t; + int r; + + assert_return(page, -EINVAL); + + /* + * First make sure the first MIN(page->n_lines, rows) lines have at + * least the required width of @cols. This does not modify any visible + * cells in the existing @page->width x @page->height area, therefore, + * we can safely bail out afterwards in case anything else fails. + * Note that lines in between page->height and page->n_lines might be + * shorter than page->width. Hence, we need to resize them all, but we + * can skip some of them for better performance. + */ + min_lines = MIN(page->n_lines, rows); + for (i = 0; i < min_lines; ++i) { + /* lines below page->height have at least page->width cells */ + if (cols < page->width && i < page->height) + continue; + + r = term_line_reserve(page->lines[i], + cols, + attr, + age, + (i < page->height) ? page->width : 0); + if (r < 0) + return r; + } + + /* + * We now know the first @min_lines lines have at least width @cols and + * are prepared for resizing. We now only have to allocate any + * additional lines below @min_lines in case @rows is greater than + * page->n_lines. + */ + if (rows > page->n_lines) { + t = realloc_multiply(page->lines, sizeof(*t), rows); + if (!t) + return -ENOMEM; + page->lines = t; + + t = realloc_multiply(page->line_cache, sizeof(*t), rows); + if (!t) + return -ENOMEM; + page->line_cache = t; + + while (page->n_lines < rows) { + r = term_line_new(&line); + if (r < 0) + return r; + + r = term_line_reserve(line, cols, attr, age, 0); + if (r < 0) + return r; + + page->lines[page->n_lines++] = line; + line = NULL; + } + } + + return 0; +} + +/** + * term_page_resize() - Resize page + * @page: page to modify + * @cols: number of columns (width) + * @rows: number of rows (height) + * @attr: attributes for newly allocated cells + * @age: age to set on any modified cells + * @history: history buffer to use for new/old lines or NULL + * + * This changes the visible dimensions of a page. You must have called + * term_page_reserve() beforehand, otherwise, this will fail. + * + * Returns: 0 on success, negative error code on failure. + */ +void term_page_resize(term_page *page, unsigned int cols, unsigned int rows, const term_attr *attr, term_age_t age, term_history *history) { + unsigned int i, num, empty, max, old_height; + term_line *line; + + assert(page); + assert(page->n_lines >= rows); + + old_height = page->height; + + if (rows < old_height) { + /* + * If we decrease the terminal-height, we emulate a scroll-up. + * This way, existing data from the scroll-area is moved into + * the history, making space at the bottom to reduce the screen + * height. In case the scroll-fill indicates empty lines, we + * reduce the amount of scrolled lines. + * Once scrolled, we have to move the lower margin from below + * the scroll area up so it is preserved. + */ + + /* move lines to history if scroll region is filled */ + num = old_height - rows; + empty = page->scroll_num - page->scroll_fill; + if (num > empty) + page_scroll_up(page, + cols, + num - empty, + attr, + age, + history); + + /* move lower margin up; drop its lines if not enough space */ + num = LESS_BY(old_height, page->scroll_idx + page->scroll_num); + max = LESS_BY(rows, page->scroll_idx); + num = MIN(num, max); + if (num > 0) { + unsigned int top, bottom; + + top = rows - num; + bottom = page->scroll_idx + page->scroll_num; + + /* might overlap; must run topdown, not bottomup */ + for (i = 0; i < num; ++i) { + line = page->lines[top + i]; + page->lines[top + i] = page->lines[bottom + i]; + page->lines[bottom + i] = line; + } + } + + /* update vertical extents */ + page->height = rows; + page->scroll_idx = MIN(page->scroll_idx, rows); + page->scroll_num -= MIN(page->scroll_num, old_height - rows); + /* fill is already up-to-date or 0 due to scroll-up */ + } else if (rows > old_height) { + /* + * If we increase the terminal-height, we emulate a scroll-down + * and fetch new lines from the history. + * New lines are always accounted to the scroll-region. Thus we + * have to preserve the lower margin first, by moving it down. + */ + + /* move lower margin down */ + num = LESS_BY(old_height, page->scroll_idx + page->scroll_num); + if (num > 0) { + unsigned int top, bottom; + + top = page->scroll_idx + page->scroll_num; + bottom = top + (rows - old_height); + + /* might overlap; must run bottomup, not topdown */ + for (i = num; i-- > 0; ) { + line = page->lines[top + i]; + page->lines[top + i] = page->lines[bottom + i]; + page->lines[bottom + i] = line; + } + } + + /* update vertical extents */ + page->height = rows; + page->scroll_num = MIN(LESS_BY(rows, page->scroll_idx), + page->scroll_num + (rows - old_height)); + + /* check how many lines can be received from history */ + if (history) + num = term_history_peek(history, + rows - old_height, + cols, + attr, + age); + else + num = 0; + + /* retrieve new lines from history if available */ + if (num > 0) + page_scroll_down(page, + cols, + num, + attr, + age, + history); + } + + /* set horizontal extents */ + page->width = cols; + for (i = 0; i < page->height; ++i) + term_line_set_width(page->lines[i], cols); +} + +/** + * term_page_write() - Write to a single cell + * @page: page to operate on + * @pos_x: x-position of cell to write to + * @pos_y: y-position of cell to write to + * @ch: character to write + * @cwidth: character-width of @ch + * @attr: attributes to set on the cell or NULL + * @age: age to use for all modifications + * @insert_mode: true if INSERT-MODE is enabled + * + * This writes a character to a specific cell. If the cell is beyond bounds, + * this is a no-op. @attr and @age are used to update the cell. @flags can be + * used to alter the behavior of this function. + * + * This is a wrapper around term_line_write(). + * + * This call does not wrap around lines. That is, this only operates on a single + * line. + */ +void term_page_write(term_page *page, unsigned int pos_x, unsigned int pos_y, term_char_t ch, unsigned int cwidth, const term_attr *attr, term_age_t age, bool insert_mode) { + assert(page); + + if (pos_y >= page->height) + return; + + term_line_write(page->lines[pos_y], pos_x, ch, cwidth, attr, age, insert_mode); +} + +/** + * term_page_insert_cells() - Insert cells into a line + * @page: page to operate on + * @from_x: x-position where to insert new cells + * @from_y: y-position where to insert new cells + * @num: number of cells to insert + * @attr: attributes to set on new cells or NULL + * @age: age to use for all modifications + * + * This inserts new cells into a given line. This is a wrapper around + * term_line_insert(). + * + * This call does not wrap around lines. That is, this only operates on a single + * line. + */ +void term_page_insert_cells(term_page *page, unsigned int from_x, unsigned int from_y, unsigned int num, const term_attr *attr, term_age_t age) { + assert(page); + + if (from_y >= page->height) + return; + + term_line_insert(page->lines[from_y], from_x, num, attr, age); +} + +/** + * term_page_delete_cells() - Delete cells from a line + * @page: page to operate on + * @from_x: x-position where to delete cells + * @from_y: y-position where to delete cells + * @num: number of cells to delete + * @attr: attributes to set on new cells or NULL + * @age: age to use for all modifications + * + * This deletes cells from a given line. This is a wrapper around + * term_line_delete(). + * + * This call does not wrap around lines. That is, this only operates on a single + * line. + */ +void term_page_delete_cells(term_page *page, unsigned int from_x, unsigned int from_y, unsigned int num, const term_attr *attr, term_age_t age) { + assert(page); + + if (from_y >= page->height) + return; + + term_line_delete(page->lines[from_y], from_x, num, attr, age); +} + +/** + * term_page_append_combchar() - Append combining-character to a cell + * @page: page to operate on + * @pos_x: x-position of target cell + * @pos_y: y-position of target cell + * @ucs4: combining character to append + * @age: age to use for all modifications + * + * This appends a combining-character to a specific cell. This is a wrapper + * around term_line_append_combchar(). + */ +void term_page_append_combchar(term_page *page, unsigned int pos_x, unsigned int pos_y, uint32_t ucs4, term_age_t age) { + assert(page); + + if (pos_y >= page->height) + return; + + term_line_append_combchar(page->lines[pos_y], pos_x, ucs4, age); +} + +/** + * term_page_erase() - Erase parts of a page + * @page: page to operate on + * @from_x: x-position where to start erasure (inclusive) + * @from_y: y-position where to start erasure (inclusive) + * @to_x: x-position where to stop erasure (inclusive) + * @to_y: y-position where to stop erasure (inclusive) + * @attr: attributes to set on cells + * @age: age to use for all modifications + * @keep_protected: true if protected cells should be kept + * + * This erases all cells starting at @from_x/@from_y up to @to_x/@to_y. Note + * that this wraps around line-boundaries so lines between @from_y and @to_y + * are cleared entirely. + * + * Lines outside the visible area are left untouched. + */ +void term_page_erase(term_page *page, unsigned int from_x, unsigned int from_y, unsigned int to_x, unsigned int to_y, const term_attr *attr, term_age_t age, bool keep_protected) { + unsigned int i, from, to; + + assert(page); + + for (i = from_y; i <= to_y && i < page->height; ++i) { + from = 0; + to = page->width; + + if (i == from_y) + from = from_x; + if (i == to_y) + to = to_x; + + term_line_erase(page->lines[i], + from, + LESS_BY(to, from), + attr, + age, + keep_protected); + } +} + +/** + * term_page_reset() - Reset page + * @page: page to modify + * @attr: attributes to set on cells + * @age: age to use for all modifications + * + * This erases the whole visible page. See term_page_erase(). + */ +void term_page_reset(term_page *page, const term_attr *attr, term_age_t age) { + assert(page); + + return term_page_erase(page, + 0, 0, + page->width - 1, page->height - 1, + attr, + age, + 0); +} + +/** + * term_page_set_scroll_region() - Set scroll region + * @page: page to operate on + * @idx: start-index of scroll region + * @num: number of lines in scroll region + * + * This sets the scroll region of a page. Whenever an operation needs to scroll + * lines, it scrolls them inside of that region. Lines outside the region are + * left untouched. In case a scroll-operation is targeted outside of this + * region, it will implicitly get a scroll-region of only one line (i.e., no + * scroll region at all). + * + * Note that the scroll-region is clipped to the current page-extents. Growing + * or shrinking the page always accounts new/old lines to the scroll region and + * moves top/bottom margins accordingly so they're preserved. + */ +void term_page_set_scroll_region(term_page *page, unsigned int idx, unsigned int num) { + assert(page); + + if (page->height < 1) { + page->scroll_idx = 0; + page->scroll_num = 0; + } else { + page->scroll_idx = MIN(idx, page->height - 1); + page->scroll_num = MIN(num, page->height - page->scroll_idx); + } +} + +/** + * term_page_scroll_up() - Scroll up + * @page: page to operate on + * @num: number of lines to scroll up + * @attr: attributes to set on new lines + * @age: age to use for all modifications + * @history: history to use for old lines or NULL + * + * This scrolls the scroll-region by @num lines. New lines are cleared and reset + * with the given attributes. Old lines are moved into the history if non-NULL. + * + * If the scroll-region is empty, this is a no-op. + */ +void term_page_scroll_up(term_page *page, unsigned int num, const term_attr *attr, term_age_t age, term_history *history) { + page_scroll_up(page, page->width, num, attr, age, history); +} + +/** + * term_page_scroll_down() - Scroll down + * @page: page to operate on + * @num: number of lines to scroll down + * @attr: attributes to set on new lines + * @age: age to use for all modifications + * @history: history to use for new lines or NULL + * + * This scrolls the scroll-region by @num lines. New lines are retrieved from + * the history or cleared if the history is empty or NULL. + * + * Usually, scroll-down implies that new lines are cleared. Therefore, you're + * highly encouraged to set @history to NULL. However, if you resize a terminal, + * you might want to include history-lines in the new area. In that case, you + * should set @history to non-NULL. + * + * If the scroll-region is empty, this is a no-op. + */ +void term_page_scroll_down(term_page *page, unsigned int num, const term_attr *attr, term_age_t age, term_history *history) { + page_scroll_down(page, page->width, num, attr, age, history); +} + +/** + * term_page_insert_lines() - Insert new lines + * @page: page to operate on + * @pos_y: y-position where to insert new lines + * @num: number of lines to insert + * @attr: attributes to set on new lines + * @age: age to use for all modifications + * + * This inserts @num new lines at position @pos_y. If @pos_y is beyond + * boundaries or @num is 0, this is a no-op. + * All lines below @pos_y are moved down to make space for the new lines. Lines + * on the bottom are dropped. Note that this only moves lines above or inside + * the scroll-region. If @pos_y is below the scroll-region, a scroll-region of + * one line is implied (which means the line is simply cleared). + */ +void term_page_insert_lines(term_page *page, unsigned int pos_y, unsigned int num, const term_attr *attr, term_age_t age) { + unsigned int scroll_idx, scroll_num; + + assert(page); + + if (pos_y >= page->height) + return; + if (num >= page->height) + num = page->height; + + /* remember scroll-region */ + scroll_idx = page->scroll_idx; + scroll_num = page->scroll_num; + + /* set scroll-region temporarily so we can reuse scroll_down() */ + { + page->scroll_idx = pos_y; + if (pos_y >= scroll_idx + scroll_num) + page->scroll_num = 1; + else if (pos_y >= scroll_idx) + page->scroll_num -= pos_y - scroll_idx; + else + page->scroll_num += scroll_idx - pos_y; + + term_page_scroll_down(page, num, attr, age, NULL); + } + + /* reset scroll-region */ + page->scroll_idx = scroll_idx; + page->scroll_num = scroll_num; +} + +/** + * term_page_delete_lines() - Delete lines + * @page: page to operate on + * @pos_y: y-position where to delete lines + * @num: number of lines to delete + * @attr: attributes to set on new lines + * @age: age to use for all modifications + * + * This deletes @num lines at position @pos_y. If @pos_y is beyond boundaries or + * @num is 0, this is a no-op. + * All lines below @pos_y are moved up into the newly made space. New lines + * on the bottom are clear. Note that this only moves lines above or inside + * the scroll-region. If @pos_y is below the scroll-region, a scroll-region of + * one line is implied (which means the line is simply cleared). + */ +void term_page_delete_lines(term_page *page, unsigned int pos_y, unsigned int num, const term_attr *attr, term_age_t age) { + unsigned int scroll_idx, scroll_num; + + assert(page); + + if (pos_y >= page->height) + return; + if (num >= page->height) + num = page->height; + + /* remember scroll-region */ + scroll_idx = page->scroll_idx; + scroll_num = page->scroll_num; + + /* set scroll-region temporarily so we can reuse scroll_up() */ + { + page->scroll_idx = pos_y; + if (pos_y >= scroll_idx + scroll_num) + page->scroll_num = 1; + else if (pos_y > scroll_idx) + page->scroll_num -= pos_y - scroll_idx; + else + page->scroll_num += scroll_idx - pos_y; + + term_page_scroll_up(page, num, attr, age, NULL); + } + + /* reset scroll-region */ + page->scroll_idx = scroll_idx; + page->scroll_num = scroll_num; +} + +/** + * term_history_new() - Create new history object + * @out: storage for pointer to new history + * + * Create a new history object. Histories are used to store scrollback-lines + * from VTE pages. You're highly recommended to set a history-limit on + * history->max_lines and trim it via term_history_trim(), otherwise history + * allocations are unlimited. + * + * Returns: 0 on success, negative error code on failure. + */ +int term_history_new(term_history **out) { + _term_history_free_ term_history *history = NULL; + + assert_return(out, -EINVAL); + + history = new0(term_history, 1); + if (!history) + return -ENOMEM; + + history->max_lines = 4096; + + *out = history; + history = NULL; + return 0; +} + +/** + * term_history_free() - Free history + * @history: history to free + * + * Clear and free history. You must not access the object afterwards. + * + * Returns: NULL + */ +term_history *term_history_free(term_history *history) { + if (!history) + return NULL; + + term_history_clear(history); + free(history); + return NULL; +} + +/** + * term_history_clear() - Clear history + * @history: history to clear + * + * Remove all linked lines from a history and reset it to its initial state. + */ +void term_history_clear(term_history *history) { + return term_history_trim(history, 0); +} + +/** + * term_history_trim() - Trim history + * @history: history to trim + * @max: maximum number of lines to be left in history + * + * This removes lines from the history until it is smaller than @max. Lines are + * removed from the top. + */ +void term_history_trim(term_history *history, unsigned int max) { + term_line *line; + + if (!history) + return; + + while (history->n_lines > max && (line = history->lines_first)) { + TERM_LINE_UNLINK(line, history); + term_line_free(line); + --history->n_lines; + } +} + +/** + * term_history_push() - Push line into history + * @history: history to work on + * @line: line to push into history + * + * This pushes a line into the given history. It is linked at the tail. In case + * the history is limited, the top-most line might be freed. + */ +void term_history_push(term_history *history, term_line *line) { + assert(history); + assert(line); + + TERM_LINE_LINK_TAIL(line, history); + if (history->max_lines > 0 && history->n_lines >= history->max_lines) { + line = history->lines_first; + TERM_LINE_UNLINK(line, history); + term_line_free(line); + } else { + ++history->n_lines; + } +} + +/** + * term_history_pop() - Retrieve last line from history + * @history: history to work on + * @new_width: width to reserve and set on the line + * @attr: attributes to use for cell reservation + * @age: age to use for cell reservation + * + * This unlinks the last linked line of the history and returns it. This also + * makes sure the line has the given width pre-allocated (see + * term_line_reserve()). If the pre-allocation fails, this returns NULL, so it + * is treated like there's no line in history left. This simplifies + * history-handling on the caller's side in case of allocation errors. No need + * to throw lines away just because the reservation failed. We can keep them in + * history safely, and make them available as scrollback. + * + * Returns: Line from history or NULL + */ +term_line *term_history_pop(term_history *history, unsigned int new_width, const term_attr *attr, term_age_t age) { + term_line *line; + int r; + + assert_return(history, NULL); + + line = history->lines_last; + if (!line) + return NULL; + + r = term_line_reserve(line, new_width, attr, age, line->width); + if (r < 0) + return NULL; + + term_line_set_width(line, new_width); + TERM_LINE_UNLINK(line, history); + --history->n_lines; + + return line; +} + +/** + * term_history_peek() - Return number of available history-lines + * @history: history to work on + * @max: maximum number of lines to look at + * @reserve_width: width to reserve on the line + * @attr: attributes to use for cell reservation + * @age: age to use for cell reservation + * + * This returns the number of available lines in the history given as @history. + * It returns at most @max. For each line that is looked at, the line is + * verified to have at least @reserve_width cells. Valid cells are preserved, + * new cells are initialized with @attr and @age. In case an allocation fails, + * we bail out and return the number of lines that are valid so far. + * + * Usually, this function should be used before running a loop on + * term_history_pop(). This function guarantees that term_history_pop() (with + * the same arguments) will succeed at least the returned number of times. + * + * Returns: Number of valid lines that can be received via term_history_pop(). + */ +unsigned int term_history_peek(term_history *history, unsigned int max, unsigned int reserve_width, const term_attr *attr, term_age_t age) { + unsigned int num; + term_line *line; + int r; + + assert(history); + + num = 0; + line = history->lines_last; + + while (num < max && line) { + r = term_line_reserve(line, reserve_width, attr, age, line->width); + if (r < 0) + break; + + ++num; + line = line->lines_prev; + } + + return num; +} |