chiark / gitweb /
Remove src/libsystemd-terminal
[elogind.git] / src / libsystemd-terminal / term-page.c
diff --git a/src/libsystemd-terminal/term-page.c b/src/libsystemd-terminal/term-page.c
deleted file mode 100644 (file)
index cbd8006..0000000
+++ /dev/null
@@ -1,2095 +0,0 @@
-/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
-
-/***
-  This file is part of systemd.
-
-  Copyright (C) 2014 David Herrmann <dh.herrmann@gmail.com>
-
-  systemd is free software; you can redistribute it and/or modify it
-  under the terms of the GNU Lesser General Public License as published by
-  the Free Software Foundation; either version 2.1 of the License, or
-  (at your option) any later version.
-
-  systemd is distributed in the hope that it will be useful, but
-  WITHOUT ANY WARRANTY; without even the implied warranty of
-  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-  Lesser General Public License for more details.
-
-  You should have received a copy of the GNU Lesser General Public License
-  along with systemd; If not, see <http://www.gnu.org/licenses/>.
-***/
-
-/*
- * Terminal Page/Line/Cell/Char Handling
- * This file implements page handling of a terminal. It is split into pages,
- * lines, cells and characters. Each object is independent of the next upper
- * object.
- *
- * The Terminal layer keeps each line of a terminal separate and dynamically
- * allocated. This allows us to move lines from main-screen to history-buffers
- * very fast. Same is true for scrolling, top/bottom borders and other buffer
- * operations.
- *
- * While lines are dynamically allocated, cells are not. This would be a waste
- * of memory and causes heavy fragmentation. Furthermore, cells are moved much
- * less frequently than lines so the performance-penalty is pretty small.
- * However, to support combining-characters, we have to initialize and cleanup
- * cells properly and cannot just release the underlying memory. Therefore,
- * cells are treated as proper objects despite being allocated in arrays.
- *
- * Each cell has a set of attributes and a stored character. This is usually a
- * single Unicode character stored as 32bit UCS-4 char. However, we need to
- * support Unicode combining-characters, therefore this gets more complicated.
- * Characters themselves are represented by a "term_char_t" object. It
- * should be treated as a normal integer and passed by value. The
- * surrounding struct is just to hide the internals. A term-char can contain a
- * base character together with up to 2 combining-chars in a single integer.
- * Only if you need more combining-chars (very unlikely!) a term-char is a
- * pointer to an allocated storage. This requires you to always free term-char
- * objects once no longer used (even though this is a no-op most of the time).
- * Furthermore, term-char objects are not ref-counted so you must duplicate them
- * in case you want to store it somewhere and retain a copy yourself. By
- * convention, all functions that take a term-char object will not duplicate
- * it but implicitly take ownership of the passed value. It's up to the caller
- * to duplicate it beforehand, in case it wants to retain a copy.
- *
- * If it turns out, that more than 2 comb-chars become common in specific
- * languages, we can try to optimize this. One idea is to ref-count allocated
- * characters and store them in a hash-table (like gnome's libvte3 does). This
- * way we will never have two allocated chars for the same content. Or we can
- * simply put two uint64_t into a "term_char_t". This will slow down operations
- * on systems that don't need that many comb-chars, but avoid the dynamic
- * 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>
-#include <stdint.h>
-#include <stdlib.h>
-#include "macro.h"
-#include "term-internal.h"
-#include "util.h"
-
-/* maximum UCS-4 character */
-#define CHAR_UCS4_MAX (0x10ffff)
-/* mask for valid UCS-4 characters (21bit) */
-#define CHAR_UCS4_MASK (0x1fffff)
-/* UCS-4 replacement character */
-#define CHAR_UCS4_REPLACEMENT (0xfffd)
-
-/* real storage behind "term_char_t" in case it's not packed */
-typedef struct term_character {
-        uint8_t n;
-        uint32_t codepoints[];
-} term_character;
-
-/*
- * char_pack() takes 3 UCS-4 values and packs them into a term_char_t object.
- * Note that UCS-4 chars only take 21 bits, so we still have the LSB as marker.
- * We set it to 1 so others can distinguish it from pointers.
- */
-static inline term_char_t char_pack(uint32_t v1, uint32_t v2, uint32_t v3) {
-        uint64_t packed, u1, u2, u3;
-
-        u1 = v1;
-        u2 = v2;
-        u3 = v3;
-
-        packed = 0x01;
-        packed |= (u1 & (uint64_t)CHAR_UCS4_MASK) << 43;
-        packed |= (u2 & (uint64_t)CHAR_UCS4_MASK) << 22;
-        packed |= (u3 & (uint64_t)CHAR_UCS4_MASK) <<  1;
-
-        return TERM_CHAR_INIT(packed);
-}
-
-#define char_pack1(_v1) char_pack2((_v1), CHAR_UCS4_MAX + 1)
-#define char_pack2(_v1, _v2) char_pack3((_v1), (_v2), CHAR_UCS4_MAX + 1)
-#define char_pack3(_v1, _v2, _v3) char_pack((_v1), (_v2), (_v3))
-
-/*
- * char_unpack() is the inverse of char_pack(). It extracts the 3 stored UCS-4
- * characters and returns them. Note that this does not validate the passed
- * term_char_t. That's the responsibility of the caller.
- * This returns the number of characters actually packed. This obviously is a
- * number between 0 and 3 (inclusive).
- */
-static inline uint8_t char_unpack(term_char_t packed, uint32_t *out_v1, uint32_t *out_v2, uint32_t *out_v3) {
-        uint32_t v1, v2, v3;
-
-        v1 = (packed._value >> 43) & (uint64_t)CHAR_UCS4_MASK;
-        v2 = (packed._value >> 22) & (uint64_t)CHAR_UCS4_MASK;
-        v3 = (packed._value >>  1) & (uint64_t)CHAR_UCS4_MASK;
-
-        if (out_v1)
-                *out_v1 = v1;
-        if (out_v2)
-                *out_v2 = v2;
-        if (out_v3)
-                *out_v3 = v3;
-
-        return (v1 > CHAR_UCS4_MAX) ? 0 :
-              ((v2 > CHAR_UCS4_MAX) ? 1 :
-              ((v3 > CHAR_UCS4_MAX) ? 2 :
-                                      3));
-}
-
-/* cast a term_char_t to a term_character* */
-static inline term_character *char_to_ptr(term_char_t ch) {
-        return (term_character*)(unsigned long)ch._value;
-}
-
-/* cast a term_character* to a term_char_t */
-static inline term_char_t char_from_ptr(term_character *c) {
-        return TERM_CHAR_INIT((unsigned long)c);
-}
-
-/*
- * char_alloc() allocates a properly aligned term_character object and returns
- * a pointer to it. NULL is returned on allocation errors. The object will have
- * enough room for @n following UCS-4 chars.
- * Note that we allocate (n+1) characters and set the last one to 0 in case
- * anyone prints this string for debugging.
- */
-static term_character *char_alloc(uint8_t n) {
-        term_character *c;
-        int r;
-
-        r = posix_memalign((void**)&c,
-                           MAX(sizeof(void*), (size_t)2),
-                           sizeof(*c) + sizeof(*c->codepoints) * (n + 1));
-        if (r)
-                return NULL;
-
-        c->n = n;
-        c->codepoints[n] = 0;
-
-        return c;
-}
-
-/*
- * char_free() frees the memory allocated via char_alloc(). It is safe to call
- * this on any term_char_t, only allocated characters are freed.
- */
-static inline void char_free(term_char_t ch) {
-        if (term_char_is_allocated(ch))
-                free(char_to_ptr(ch));
-}
-
-/*
- * This appends @append_ucs4 to the existing character @base and returns
- * it as a new character. In case that's not possible, @base is returned. The
- * caller can use term_char_same() to test whether the returned character was
- * freshly allocated or not.
- */
-static term_char_t char_build(term_char_t base, uint32_t append_ucs4) {
-        /* soft-limit for combining-chars; hard-limit is currently 255 */
-        const size_t climit = 64;
-        term_character *c;
-        uint32_t buf[3], *t;
-        uint8_t n;
-
-        /* ignore invalid UCS-4 */
-        if (append_ucs4 > CHAR_UCS4_MAX)
-                return base;
-
-        if (term_char_is_null(base)) {
-                return char_pack1(append_ucs4);
-        } else if (!term_char_is_allocated(base)) {
-                /* unpack and try extending the packed character */
-                n = char_unpack(base, &buf[0], &buf[1], &buf[2]);
-
-                switch (n) {
-                case 0:
-                        return char_pack1(append_ucs4);
-                case 1:
-                        if (climit < 2)
-                                return base;
-
-                        return char_pack2(buf[0], append_ucs4);
-                case 2:
-                        if (climit < 3)
-                                return base;
-
-                        return char_pack3(buf[0], buf[1], append_ucs4);
-                default:
-                        /* fallthrough */
-                        break;
-                }
-
-                /* already fully packed, we need to allocate a new one */
-                t = buf;
-        } else {
-                /* already an allocated type, we need to allocate a new one */
-                c = char_to_ptr(base);
-                t = c->codepoints;
-                n = c->n;
-        }
-
-        /* bail out if soft-limit is reached */
-        if (n >= climit)
-                return base;
-
-        /* allocate new char */
-        c = char_alloc(n + 1);
-        if (!c)
-                return base;
-
-        memcpy(c->codepoints, t, sizeof(*t) * n);
-        c->codepoints[n] = append_ucs4;
-
-        return char_from_ptr(c);
-}
-
-/**
- * term_char_set() - Reset character to a single UCS-4 character
- * @previous: term-char to reset
- * @append_ucs4: UCS-4 char to set
- *
- * This frees all resources in @previous and re-initializes it to @append_ucs4.
- * The new char is returned.
- *
- * Usually, this is used like this:
- *   obj->ch = term_char_set(obj->ch, ucs4);
- *
- * Returns: The previous character reset to @append_ucs4.
- */
-term_char_t term_char_set(term_char_t previous, uint32_t append_ucs4) {
-        char_free(previous);
-        return char_build(TERM_CHAR_NULL, append_ucs4);
-}
-
-/**
- * term_char_merge() - Merge UCS-4 char at the end of an existing char
- * @base: existing term-char
- * @append_ucs4: UCS-4 character to append
- *
- * This appends @append_ucs4 to @base and returns the result. @base is
- * invalidated by this function and must no longer be used. The returned value
- * replaces the old one.
- *
- * Usually, this is used like this:
- *   obj->ch = term_char_merge(obj->ch, ucs4);
- *
- * Returns: The new merged character.
- */
-term_char_t term_char_merge(term_char_t base, uint32_t append_ucs4) {
-        term_char_t ch;
-
-        ch = char_build(base, append_ucs4);
-        if (!term_char_same(ch, base))
-                term_char_free(base);
-
-        return ch;
-}
-
-/**
- * term_char_dup() - Duplicate character
- * @ch: character to duplicate
- *
- * This duplicates a term-character. In case the character is not allocated,
- * nothing is done. Otherwise, the underlying memory is copied and returned. You
- * need to call term_char_free() on the returned character to release it again.
- * On allocation errors, a replacement character is returned. Therefore, the
- * caller can safely assume that this function always succeeds.
- *
- * Returns: The duplicated term-character.
- */
-term_char_t term_char_dup(term_char_t ch) {
-        term_character *c, *newc;
-
-        if (!term_char_is_allocated(ch))
-                return ch;
-
-        c = char_to_ptr(ch);
-        newc = char_alloc(c->n);
-        if (!newc)
-                return char_pack1(CHAR_UCS4_REPLACEMENT);
-
-        memcpy(newc->codepoints, c->codepoints, sizeof(*c->codepoints) * c->n);
-        return char_from_ptr(newc);
-}
-
-/**
- * term_char_dup_append() - Duplicate tsm-char with UCS-4 character appended
- * @base: existing term-char
- * @append_ucs4: UCS-4 character to append
- *
- * This is similar to term_char_merge(), but it returns a separately allocated
- * character. That is, @base will stay valid after this returns and is not
- * touched. In case the append-operation fails, @base is duplicated and
- * returned. That is, the returned char is always independent of @base.
- *
- * Returns: Newly allocated character with @append_ucs4 appended to @base.
- */
-term_char_t term_char_dup_append(term_char_t base, uint32_t append_ucs4) {
-        term_char_t ch;
-
-        ch = char_build(base, append_ucs4);
-        if (term_char_same(ch, base))
-                ch = term_char_dup(base);
-
-        return ch;
-}
-
-/**
- * term_char_resolve() - Retrieve the UCS-4 string for a term-char
- * @ch: character to resolve
- * @s: storage for size of string or NULL
- * @b: storage for string or NULL
- *
- * This takes a term-character and returns the UCS-4 string associated with it.
- * In case @ch is not allocated, the string is stored in @b (in case @b is NULL
- * static storage is used). Otherwise, a pointer to the allocated storage is
- * returned.
- *
- * The returned string is only valid as long as @ch and @b are valid. The string
- * is zero-terminated and can safely be printed via long-character printf().
- * The length of the string excluding the zero-character is returned in @s.
- *
- * This never returns NULL. Even if the size is 0, this points to a buffer of at
- * least a zero-terminator.
- *
- * Returns: The UCS-4 string-representation of @ch, and its size in @s.
- */
-const uint32_t *term_char_resolve(term_char_t ch, size_t *s, term_charbuf_t *b) {
-        static term_charbuf_t static_b;
-        term_character *c;
-        uint32_t *cache;
-        size_t len;
-
-        if (b)
-                cache = b->buf;
-        else
-                cache = static_b.buf;
-
-        if (term_char_is_null(ch)) {
-                len = 0;
-                cache[0] = 0;
-        } else if (term_char_is_allocated(ch)) {
-                c = char_to_ptr(ch);
-                len = c->n;
-                cache = c->codepoints;
-        } else {
-                len = char_unpack(ch, &cache[0], &cache[1], &cache[2]);
-                cache[len] = 0;
-        }
-
-        if (s)
-                *s = len;
-
-        return cache;
-}
-
-/**
- * term_char_lookup_width() - Lookup cell-width of a character
- * @ch: character to return cell-width for
- *
- * This is an equivalent of wcwidth() for term_char_t. It can deal directly
- * with UCS-4 and combining-characters and avoids the mess that is wchar_t and
- * locale handling.
- *
- * Returns: 0 for unprintable characters, >0 for everything else.
- */
-unsigned int term_char_lookup_width(term_char_t ch) {
-        term_charbuf_t b;
-        const uint32_t *str;
-        unsigned int max;
-        size_t i, len;
-        int r;
-
-        max = 0;
-        str = term_char_resolve(ch, &len, &b);
-
-        for (i = 0; i < len; ++i) {
-                /*
-                 * Oh god, C99 locale handling strikes again: wcwidth() expects
-                 * wchar_t, but there is no way for us to know the
-                 * internal encoding of wchar_t. Moreover, it is nearly
-                 * impossible to convert UCS-4 into wchar_t (except for iconv,
-                 * which is way too much overhead).
-                 * Therefore, we use our own copy of wcwidth(). Lets just hope
-                 * that glibc will one day export it's internal UCS-4 and UTF-8
-                 * helpers for direct use.
-                 */
-                assert_cc(sizeof(wchar_t) >= 4);
-                r = mk_wcwidth((wchar_t)str[i]);
-                if (r > 0 && (unsigned int)r > max)
-                        max = r;
-        }
-
-        return max;
-}
-
-/**
- * term_cell_init() - Initialize a new cell
- * @cell: cell to initialize
- * @ch: character to set on the cell or TERM_CHAR_NULL
- * @cwidth: character width of @ch
- * @attr: attributes to set on the cell or NULL
- * @age: age to set on the cell or TERM_AGE_NULL
- *
- * This initializes a new cell. The backing-memory of the cell must be allocated
- * by the caller beforehand. The caller is responsible to destroy the cell via
- * term_cell_destroy() before freeing the backing-memory.
- *
- * It is safe (and supported!) to use:
- *   zero(*c);
- * instead of:
- *   term_cell_init(c, TERM_CHAR_NULL, NULL, TERM_AGE_NULL);
- *
- * Note that this call takes ownership of @ch. If you want to use it yourself
- * after this call, you need to duplicate it before calling this.
- */
-static void term_cell_init(term_cell *cell, term_char_t ch, unsigned int cwidth, const term_attr *attr, term_age_t age) {
-        assert(cell);
-
-        cell->ch = ch;
-        cell->cwidth = cwidth;
-        cell->age = age;
-
-        if (attr)
-                memcpy(&cell->attr, attr, sizeof(*attr));
-        else
-                zero(cell->attr);
-}
-
-/**
- * term_cell_destroy() - Destroy previously initialized cell
- * @cell: cell to destroy or NULL
- *
- * This releases all resources associated with a cell. The backing memory is
- * kept as-is. It's the responsibility of the caller to manage it.
- *
- * You must not call any other cell operations on this cell after this call
- * returns. You must re-initialize the cell via term_cell_init() before you can
- * use it again.
- *
- * If @cell is NULL, this is a no-op.
- */
-static void term_cell_destroy(term_cell *cell) {
-        if (!cell)
-                return;
-
-        term_char_free(cell->ch);
-}
-
-/**
- * term_cell_set() - Change contents of a cell
- * @cell: cell to modify
- * @ch: character to set on the cell or cell->ch
- * @cwidth: character width of @ch or cell->cwidth
- * @attr: attributes to set on the cell or NULL
- * @age: age to set on the cell or cell->age
- *
- * This changes the contents of a cell. It can be used to change the character,
- * attributes and age. To keep the current character, pass cell->ch as @ch. To
- * reset the current attributes, pass NULL. To keep the current age, pass
- * cell->age.
- *
- * This call takes ownership of @ch. You need to duplicate it first, in case you
- * want to use it for your own purposes after this call.
- *
- * The cell must have been initialized properly before calling this. See
- * term_cell_init().
- */
-static void term_cell_set(term_cell *cell, term_char_t ch, unsigned int cwidth, const term_attr *attr, term_age_t age) {
-        assert(cell);
-
-        if (!term_char_same(ch, cell->ch)) {
-                term_char_free(cell->ch);
-                cell->ch = ch;
-        }
-
-        cell->cwidth = cwidth;
-        cell->age = age;
-
-        if (attr)
-                memcpy(&cell->attr, attr, sizeof(*attr));
-        else
-                zero(cell->attr);
-}
-
-/**
- * term_cell_append() - Append a combining-char to a cell
- * @cell: cell to modify
- * @ucs4: UCS-4 character to append to the cell
- * @age: new age to set on the cell or cell->age
- *
- * This appends a combining-character to a cell. No validation of the UCS-4
- * character is done, so this can be used to append any character. Additionally,
- * this can update the age of the cell.
- *
- * The cell must have been initialized properly before calling this. See
- * term_cell_init().
- */
-static void term_cell_append(term_cell *cell, uint32_t ucs4, term_age_t age) {
-        assert(cell);
-
-        cell->ch = term_char_merge(cell->ch, ucs4);
-        cell->age = age;
-}
-
-/**
- * term_cell_init_n() - Initialize an array of cells
- * @cells: pointer to an array of cells to initialize
- * @n: number of cells
- * @attr: attributes to set on all cells or NULL
- * @age: age to set on all cells
- *
- * This is the same as term_cell_init() but initializes an array of cells.
- * Furthermore, this always sets the character to TERM_CHAR_NULL.
- * If you want to set a specific characters on all cells, you need to hard-code
- * this loop and duplicate the character for each cell.
- */
-static void term_cell_init_n(term_cell *cells, unsigned int n, const term_attr *attr, term_age_t age) {
-        for ( ; n > 0; --n, ++cells)
-                term_cell_init(cells, TERM_CHAR_NULL, 0, attr, age);
-}
-
-/**
- * term_cell_destroy_n() - Destroy an array of cells
- * @cells: pointer to an array of cells to destroy
- * @n: number of cells
- *
- * This is the same as term_cell_destroy() but destroys an array of cells.
- */
-static void term_cell_destroy_n(term_cell *cells, unsigned int n) {
-        for ( ; n > 0; --n, ++cells)
-                term_cell_destroy(cells);
-}
-
-/**
- * term_cell_clear_n() - Clear contents of an array of cells
- * @cells: pointer to an array of cells to modify
- * @n: number of cells
- * @attr: attributes to set on all cells or NULL
- * @age: age to set on all cells
- *
- * This is the same as term_cell_set() but operates on an array of cells. Note
- * that all characters are always set to TERM_CHAR_NULL, unlike term_cell_set()
- * which takes the character as argument.
- * If you want to set a specific characters on all cells, you need to hard-code
- * this loop and duplicate the character for each cell.
- */
-static void term_cell_clear_n(term_cell *cells, unsigned int n, const term_attr *attr, term_age_t age) {
-        for ( ; n > 0; --n, ++cells)
-                term_cell_set(cells, TERM_CHAR_NULL, 0, attr, age);
-}
-
-/**
- * term_line_new() - Allocate a new line
- * @out: place to store pointer to new line
- *
- * This allocates and initialized a new line. The line is unlinked and
- * independent of any page. It can be used for any purpose. The initial
- * cell-count is set to 0.
- *
- * The line has to be freed via term_line_free() once it's no longer needed.
- *
- * Returns: 0 on success, negative error code on failure.
- */
-int term_line_new(term_line **out) {
-        _term_line_free_ term_line *line = NULL;
-
-        assert_return(out, -EINVAL);
-
-        line = new0(term_line, 1);
-        if (!line)
-                return -ENOMEM;
-
-        *out = line;
-        line = NULL;
-        return 0;
-}
-
-/**
- * term_line_free() - Free a line
- * @line: line to free or NULL
- *
- * This frees a line that was previously allocated via term_line_free(). All its
- * cells are released, too.
- *
- * If @line is NULL, this is a no-op.
- */
-term_line *term_line_free(term_line *line) {
-        if (!line)
-                return NULL;
-
-        term_cell_destroy_n(line->cells, line->n_cells);
-        free(line->cells);
-        free(line);
-
-        return NULL;
-}
-
-/**
- * term_line_reserve() - Pre-allocate cells for a line
- * @line: line to pre-allocate cells for
- * @width: numbers of cells the line shall have pre-allocated
- * @attr: attribute for all allocated cells or NULL
- * @age: current age for all modifications
- * @protect_width: width to protect from erasure
- *
- * This pre-allocates cells for this line. Please note that @width is the number
- * of cells the line is guaranteed to have allocated after this call returns.
- * It's not the number of cells that are added, neither is it the new width of
- * the line.
- *
- * This function never frees memory. That is, reducing the line-width will
- * always succeed, same is true for increasing the width to a previously set
- * width.
- *
- * @attr and @age are used to initialize new cells. Additionally, any
- * existing cell outside of the protected area specified by @protect_width are
- * cleared and reset with @attr and @age.
- *
- * Returns: 0 on success, negative error code on failure.
- */
-int term_line_reserve(term_line *line, unsigned int width, const term_attr *attr, term_age_t age, unsigned int protect_width) {
-        unsigned int min_width;
-        term_cell *t;
-
-        assert_return(line, -EINVAL);
-
-        /* reset existing cells if required */
-        min_width = MIN(line->n_cells, width);
-        if (min_width > protect_width)
-                term_cell_clear_n(line->cells + protect_width,
-                                  min_width - protect_width,
-                                  attr,
-                                  age);
-
-        /* allocate new cells if required */
-
-        if (width > line->n_cells) {
-                t = realloc_multiply(line->cells, sizeof(*t), width);
-                if (!t)
-                        return -ENOMEM;
-
-                if (!attr && !age)
-                        memzero(t + line->n_cells,
-                                sizeof(*t) * (width - line->n_cells));
-                else
-                        term_cell_init_n(t + line->n_cells,
-                                         width - line->n_cells,
-                                         attr,
-                                         age);
-
-                line->cells = t;
-                line->n_cells = width;
-        }
-
-        line->fill = MIN(line->fill, protect_width);
-
-        return 0;
-}
-
-/**
- * term_line_set_width() - Change width of a line
- * @line: line to modify
- * @width: new width
- *
- * This changes the actual width of a line. It is the caller's responsibility
- * to use term_line_reserve() to make sure enough space is allocated. If @width
- * is greater than the allocated size, it is cropped.
- *
- * This does not modify any cells. Use term_line_reserve() or term_line_erase()
- * to clear any newly added cells.
- *
- * NOTE: The fill state is cropped at line->width. Therefore, if you increase
- *       the line-width afterwards, but there is a multi-cell character at the
- *       end of the line that got cropped, then the fill-state will _not_ be
- *       adjusted.
- *       This means, the fill-state always includes the cells up to the start
- *       of the right-most character, but it might or might not cover it until
- *       its end. This should be totally fine, though. You should never access
- *       multi-cell tails directly, anyway.
- */
-void term_line_set_width(term_line *line, unsigned int width) {
-        assert(line);
-
-        if (width > line->n_cells)
-                width = line->n_cells;
-
-        line->width = width;
-        line->fill = MIN(line->fill, width);
-}
-
-/**
- * line_insert() - Insert characters and move existing cells to the right
- * @from: position to insert cells at
- * @num: number of cells to insert
- * @head_char: character that is set on the first cell
- * @head_cwidth: character-length of @head_char
- * @attr: attribute for all inserted cells or NULL
- * @age: current age for all modifications
- *
- * The INSERT operation (or writes with INSERT_MODE) writes data at a specific
- * position on a line and shifts the existing cells to the right. Cells that are
- * moved beyond the right hand border are discarded.
- *
- * This helper contains the actual INSERT implementation which is independent of
- * the data written. It works on cells, not on characters. The first cell is set
- * to @head_char, all others are reset to TERM_CHAR_NULL. See each caller for a
- * more detailed description.
- */
-static inline void line_insert(term_line *line, unsigned int from, unsigned int num, term_char_t head_char, unsigned int head_cwidth, const term_attr *attr, term_age_t age) {
-        unsigned int i, rem, move;
-
-        if (from >= line->width)
-                return;
-        if (from + num < from || from + num > line->width)
-                num = line->width - from;
-        if (!num)
-                return;
-
-        move = line->width - from - num;
-        rem = MIN(num, move);
-
-        if (rem > 0) {
-                /*
-                 * Make room for @num cells; shift cells to the right if
-                 * required. @rem is the number of remaining cells that we will
-                 * knock off on the right and overwrite during the right shift.
-                 *
-                 * For INSERT_MODE, @num/@rem are usually 1 or 2, @move is 50%
-                 * of the line on average. Therefore, the actual move is quite
-                 * heavy and we can safely invalidate cells manually instead of
-                 * the whole line.
-                 * However, for INSERT operations, any parameters are
-                 * possible. But we cannot place any assumption on its usage
-                 * across applications, so we just handle it the same as
-                 * INSERT_MODE and do per-cell invalidation.
-                 */
-
-                /* destroy cells that are knocked off on the right */
-                term_cell_destroy_n(line->cells + line->width - rem, rem);
-
-                /* move remaining bulk of cells */
-                memmove(line->cells + from + num,
-                        line->cells + from,
-                        sizeof(*line->cells) * move);
-
-                /* invalidate cells */
-                for (i = 0; i < move; ++i)
-                        line->cells[from + num + i].age = age;
-
-                /* initialize fresh head-cell */
-                term_cell_init(line->cells + from,
-                               head_char,
-                               head_cwidth,
-                               attr,
-                               age);
-
-                /* initialize fresh tail-cells */
-                term_cell_init_n(line->cells + from + 1,
-                                 num - 1,
-                                 attr,
-                                 age);
-
-                /* adjust fill-state */
-                DISABLE_WARNING_SHADOW;
-                line->fill = MIN(line->width,
-                                 MAX(line->fill + num,
-                                     from + num));
-                REENABLE_WARNING;
-        } else {
-                /* modify head-cell */
-                term_cell_set(line->cells + from,
-                              head_char,
-                              head_cwidth,
-                              attr,
-                              age);
-
-                /* reset tail-cells */
-                term_cell_clear_n(line->cells + from + 1,
-                                  num - 1,
-                                  attr,
-                                  age);
-
-                /* adjust fill-state */
-                line->fill = line->width;
-        }
-}
-
-/**
- * term_line_write() - Write to a single, specific cell
- * @line: line to write to
- * @pos_x: x-position of cell in @line to write to
- * @ch: character to write to the cell
- * @cwidth: character width of @ch
- * @attr: attributes to set on the cell or NULL
- * @age: current age for all modifications
- * @insert_mode: true if INSERT-MODE is enabled
- *
- * This writes to a specific cell in a line. The cell is addressed by its
- * X-position @pos_x. If that cell does not exist, this is a no-op.
- *
- * @ch and @attr are set on this cell.
- *
- * If @insert_mode is true, this inserts the character instead of overwriting
- * existing data (existing data is now moved to the right before writing).
- *
- * This function is the low-level handler of normal writes to a terminal.
- */
-void term_line_write(term_line *line, unsigned int pos_x, term_char_t ch, unsigned int cwidth, const term_attr *attr, term_age_t age, bool insert_mode) {
-        unsigned int len;
-
-        assert(line);
-
-        if (pos_x >= line->width)
-                return;
-
-        len = MAX(1U, cwidth);
-        if (pos_x + len < pos_x || pos_x + len > line->width)
-                len = line->width - pos_x;
-        if (!len)
-                return;
-
-        if (insert_mode) {
-                /* Use line_insert() to insert the character-head and fill
-                 * the remains with NULLs. */
-                line_insert(line, pos_x, len, ch, cwidth, attr, age);
-        } else {
-                /* modify head-cell */
-                term_cell_set(line->cells + pos_x, ch, cwidth, attr, age);
-
-                /* reset tail-cells */
-                term_cell_clear_n(line->cells + pos_x + 1,
-                                  len - 1,
-                                  attr,
-                                  age);
-
-                /* adjust fill-state */
-                DISABLE_WARNING_SHADOW;
-                line->fill = MIN(line->width,
-                                 MAX(line->fill,
-                                     pos_x + len));
-                REENABLE_WARNING;
-        }
-}
-
-/**
- * term_line_insert() - Insert empty cells
- * @line: line to insert empty cells into
- * @from: x-position where to insert cells
- * @num: number of cells to insert
- * @attr: attributes to set on the cells or NULL
- * @age: current age for all modifications
- *
- * This inserts @num empty cells at position @from in line @line. All existing
- * cells to the right are shifted to make room for the new cells. Cells that get
- * pushed beyond the right hand border are discarded.
- */
-void term_line_insert(term_line *line, unsigned int from, unsigned int num, const term_attr *attr, term_age_t age) {
-        /* use line_insert() to insert @num empty cells */
-        return line_insert(line, from, num, TERM_CHAR_NULL, 0, attr, age);
-}
-
-/**
- * term_line_delete() - Delete cells from line
- * @line: line to delete cells from
- * @from: position to delete cells at
- * @num: number of cells to delete
- * @attr: attributes to set on any new cells
- * @age: current age for all modifications
- *
- * Delete cells from a line. All cells to the right of the deleted cells are
- * shifted to the left to fill the empty space. New cells appearing on the right
- * hand border are cleared and initialized with @attr.
- */
-void term_line_delete(term_line *line, unsigned int from, unsigned int num, const term_attr *attr, term_age_t age) {
-        unsigned int rem, move, i;
-
-        assert(line);
-
-        if (from >= line->width)
-                return;
-        if (from + num < from || from + num > line->width)
-                num = line->width - from;
-        if (!num)
-                return;
-
-        /* destroy and move as many upfront as possible */
-        move = line->width - from - num;
-        rem = MIN(num, move);
-        if (rem > 0) {
-                /* destroy to be removed cells */
-                term_cell_destroy_n(line->cells + from, rem);
-
-                /* move tail upfront */
-                memmove(line->cells + from,
-                        line->cells + from + num,
-                        sizeof(*line->cells) * move);
-
-                /* invalidate copied cells */
-                for (i = 0; i < move; ++i)
-                        line->cells[from + i].age = age;
-
-                /* initialize tail that was moved away */
-                term_cell_init_n(line->cells + line->width - rem,
-                                 rem,
-                                 attr,
-                                 age);
-
-                /* reset remaining cells in case the move was too small */
-                if (num > move)
-                        term_cell_clear_n(line->cells + from + move,
-                                          num - move,
-                                          attr,
-                                          age);
-        } else {
-                /* reset cells */
-                term_cell_clear_n(line->cells + from,
-                                  num,
-                                  attr,
-                                  age);
-        }
-
-        /* adjust fill-state */
-        if (from + num < line->fill)
-                line->fill -= num;
-        else if (from < line->fill)
-                line->fill = from;
-}
-
-/**
- * term_line_append_combchar() - Append combining char to existing cell
- * @line: line to modify
- * @pos_x: position of cell to append combining char to
- * @ucs4: combining character to append
- * @age: current age for all modifications
- *
- * Unicode allows trailing combining characters, which belong to the
- * char in front of them. The caller is responsible of detecting
- * combining characters and calling term_line_append_combchar() instead of
- * term_line_write(). This simply appends the char to the correct cell then.
- * If the cell is not in the visible area, this call is skipped.
- *
- * Note that control-sequences are not 100% compatible with combining
- * characters as they require delayed parsing. However, we must handle
- * control-sequences immediately. Therefore, there might be trailing
- * combining chars that should be discarded by the parser.
- * However, to prevent programming errors, we're also being pedantic
- * here and discard weirdly placed combining chars. This prevents
- * situations were invalid content is parsed into the terminal and you
- * might end up with cells containing only combining chars.
- *
- * Long story short: To get combining-characters working with old-fashioned
- * terminal-emulation, we parse them exclusively for direct cell-writes. Other
- * combining-characters are usually simply discarded and ignored.
- */
-void term_line_append_combchar(term_line *line, unsigned int pos_x, uint32_t ucs4, term_age_t age) {
-        assert(line);
-
-        if (pos_x >= line->width)
-                return;
-
-        /* Unused cell? Skip appending any combining chars then. */
-        if (term_char_is_null(line->cells[pos_x].ch))
-                return;
-
-        term_cell_append(line->cells + pos_x, ucs4, age);
-}
-
-/**
- * term_line_erase() - Erase parts of a line
- * @line: line to modify
- * @from: position to start the erase
- * @num: number of cells to erase
- * @attr: attributes to initialize erased cells with
- * @age: current age for all modifications
- * @keep_protected: true if protected cells should be kept
- *
- * This is the standard erase operation. It clears all cells in the targeted
- * area and re-initializes them. Cells to the right are not shifted left, you
- * must use DELETE to achieve that. Cells outside the visible area are skipped.
- *
- * If @keep_protected is true, protected cells will not be erased.
- */
-void term_line_erase(term_line *line, unsigned int from, unsigned int num, const term_attr *attr, term_age_t age, bool keep_protected) {
-        term_cell *cell;
-        unsigned int i, last_protected;
-
-        assert(line);
-
-        if (from >= line->width)
-                return;
-        if (from + num < from || from + num > line->width)
-                num = line->width - from;
-        if (!num)
-                return;
-
-        last_protected = 0;
-        for (i = 0; i < num; ++i) {
-                cell = line->cells + from + i;
-                if (keep_protected && cell->attr.protect) {
-                        /* only count protected-cells inside the fill-region */
-                        if (from + i < line->fill)
-                                last_protected = from + i;
-
-                        continue;
-                }
-
-                term_cell_set(cell, TERM_CHAR_NULL, 0, attr, age);
-        }
-
-        /* Adjust fill-state. This is a bit tricks, we can only adjust it in
-         * case the erase-region starts inside the fill-region and ends at the
-         * tail or beyond the fill-region. Otherwise, the current fill-state
-         * stays as it was.
-         * Furthermore, we must account for protected cells. The loop above
-         * ensures that protected-cells are only accounted for if they're
-         * inside the fill-region. */
-        if (from < line->fill && from + num >= line->fill)
-                line->fill = MAX(from, last_protected);
-}
-
-/**
- * term_line_reset() - Reset a line
- * @line: line to reset
- * @attr: attributes to initialize all cells with
- * @age: current age for all modifications
- *
- * This resets all visible cells of a line and sets their attributes and ages
- * to @attr and @age. This is equivalent to erasing a whole line via
- * term_line_erase().
- */
-void term_line_reset(term_line *line, const term_attr *attr, term_age_t age) {
-        assert(line);
-
-        return term_line_erase(line, 0, line->width, attr, age, 0);
-}
-
-/**
- * term_line_link() - Link line in front of a list
- * @line: line to link
- * @first: member pointing to first entry
- * @last: member pointing to last entry
- *
- * This links a line into a list of lines. The line is inserted at the front and
- * must not be linked, yet. See the TERM_LINE_LINK() macro for an easier usage of
- * this.
- */
-void term_line_link(term_line *line, term_line **first, term_line **last) {
-        assert(line);
-        assert(first);
-        assert(last);
-        assert(!line->lines_prev);
-        assert(!line->lines_next);
-
-        line->lines_prev = NULL;
-        line->lines_next = *first;
-        if (*first)
-                (*first)->lines_prev = line;
-        else
-                *last = line;
-        *first = line;
-}
-
-/**
- * term_line_link_tail() - Link line at tail of a list
- * @line: line to link
- * @first: member pointing to first entry
- * @last: member pointing to last entry
- *
- * Same as term_line_link() but links the line at the tail.
- */
-void term_line_link_tail(term_line *line, term_line **first, term_line **last) {
-        assert(line);
-        assert(first);
-        assert(last);
-        assert(!line->lines_prev);
-        assert(!line->lines_next);
-
-        line->lines_next = NULL;
-        line->lines_prev = *last;
-        if (*last)
-                (*last)->lines_next = line;
-        else
-                *first = line;
-        *last = line;
-}
-
-/**
- * term_line_unlink() - Unlink line from a list
- * @line: line to unlink
- * @first: member pointing to first entry
- * @last: member pointing to last entry
- *
- * This unlinks a previously linked line. See TERM_LINE_UNLINK() for an easier to
- * use macro.
- */
-void term_line_unlink(term_line *line, term_line **first, term_line **last) {
-        assert(line);
-        assert(first);
-        assert(last);
-
-        if (line->lines_prev)
-                line->lines_prev->lines_next = line->lines_next;
-        else
-                *first = line->lines_next;
-        if (line->lines_next)
-                line->lines_next->lines_prev = line->lines_prev;
-        else
-                *last = line->lines_prev;
-
-        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;
-}