chiark / gitweb /
terminal: add page handling for terminals
authorDavid Herrmann <dh.herrmann@gmail.com>
Fri, 13 Jun 2014 17:00:29 +0000 (19:00 +0200)
committerDavid Herrmann <dh.herrmann@gmail.com>
Fri, 18 Jul 2014 10:53:41 +0000 (12:53 +0200)
The page-layer is a one-dimensional array of lines. Combined with the
one-dimensional lines, you get a two-dimensional page. However, both
implementations, lines and pages only deal with their own dimension. That
means, lines don't know anything about other lines, and pages don't know
anything about cells.

Apart from pages, this also introduces history objects. A history object
is a scroll-back buffer. As some pages like alt-buffers don't have
histories, we keep them separate.

Pages itself forward all cell-related operations to the related line. Only
line-related operations are directly handled by the page. This is mostly
scrolling and history. To support proper resizing, we also keep a
fill-state just like lines do for cells.

src/libsystemd-terminal/term-internal.h
src/libsystemd-terminal/term-page.c

index 56ebd30..d7d2f98 100644 (file)
@@ -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);
index bfff3b1..7ae90e2 100644 (file)
  * 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;
+}