chiark / gitweb /
terminal: add page handling for terminals
[elogind.git] / src / libsystemd-terminal / term-page.c
index bfff3b1719bf5769cd48b471e7b28277f518259e..7ae90e2cdaea6d3c5f4808de60609d35d8bde17e 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;
+}