HelenOS sources

root/uspace/lib/termui/src/termui.c

/* [<][>][^][v][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. _real_row
  2. termui_set_style
  3. _termui_evict_row
  4. termui_get_active_row
  5. _update_active_cells
  6. _update_current_cell
  7. _cursor_off
  8. _cursor_on
  9. _advance_line
  10. termui_put_lf
  11. termui_put_cr
  12. termui_put_crlf
  13. termui_put_tab
  14. termui_put_backspace
  15. termui_put_glyph
  16. termui_color_from_rgb
  17. termui_color_to_rgb
  18. termui_get_cols
  19. termui_get_rows
  20. termui_get_pos
  21. termui_set_pos
  22. termui_clear_screen
  23. termui_wipe_screen
  24. termui_set_scroll_cb
  25. termui_set_update_cb
  26. termui_set_refresh_cb
  27. termui_force_viewport_update
  28. termui_scrollback_is_active
  29. termui_create
  30. termui_destroy
  31. termui_history_scroll
  32. termui_set_cursor_visibility
  33. termui_get_cursor_visibility
  34. _termui_put_cells
  35. termui_resize

/*
 * Copyright (c) 2024 Jiří Zárevúcky
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 * - The name of the author may not be used to endorse or promote products
 *   derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <termui.h>

#include <assert.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>

#include "history.h"

struct termui {
        int cols;
        int rows;

        int col;
        int row;

        bool cursor_visible;

        // How much of the screen is in use. Relevant for clrscr.
        int used_rows;

        // Row index of the first screen row in the circular screen buffer.
        int first_row;
        // rows * cols circular buffer of the current virtual screen contents.
        // Does not necessarily correspond to the currently visible text,
        // if scrollback is active.
        termui_cell_t *screen;
        // Set to one if the corresponding row has overflowed into the next row.
        uint8_t *overflow_flags;

        /* Used to remove extra newline when CRLF is placed exactly on row boundary. */
        bool overflow;

        struct history history;

        termui_cell_t style;
        termui_cell_t default_cell;

        termui_scroll_cb_t scroll_cb;
        termui_update_cb_t update_cb;
        termui_refresh_cb_t refresh_cb;
        void *scroll_udata;
        void *update_udata;
        void *refresh_udata;
};

static int _real_row(const termui_t *termui, int row)
{
        row += termui->first_row;
        if (row >= termui->rows)
                row -= termui->rows;

        return row;
}

#define _screen_cell(termui, col, row) \
        ((termui)->screen[(termui)->cols * _real_row((termui), (row)) + (col)])

#define _current_cell(termui) \
        _screen_cell((termui), (termui)->col, (termui)->row)

#define _overflow_flag(termui, row) \
        ((termui)->overflow_flags[_real_row((termui), (row))])

/** Sets current cell style/color.
 */
void termui_set_style(termui_t *termui, termui_cell_t style)
{
        termui->style = style;
}

static void _termui_evict_row(termui_t *termui)
{
        if (termui->used_rows <= 0)
                return;

        bool last = !_overflow_flag(termui, 0);

        for (int col = 0; col < termui->cols; col++)
                _screen_cell(termui, col, 0).cursor = 0;

        /* Append first row of the screen to history. */
        _history_append_row(&termui->history, &_screen_cell(termui, 0, 0), last);

        _overflow_flag(termui, 0) = false;

        /* Clear the row we moved to history. */
        for (int col = 0; col < termui->cols; col++)
                _screen_cell(termui, col, 0) = termui->default_cell;

        termui->used_rows--;

        termui->row--;
        if (termui->row < 0) {
                termui->row = 0;
                termui->col = 0;
        }

        termui->first_row++;
        if (termui->first_row >= termui->rows)
                termui->first_row -= termui->rows;

        assert(termui->first_row < termui->rows);
}

/**
 * Get active screen row. This always points to the primary output buffer,
 * unaffected by viewport shifting. Can be used for modifying the screen
 * directly. For displaying viewport, use termui_force_viewport_update().
 */
termui_cell_t *termui_get_active_row(termui_t *termui, int row)
{
        assert(row >= 0);
        assert(row < termui->rows);

        return &_screen_cell(termui, 0, row);
}

static void _update_active_cells(termui_t *termui, int col, int row, int cells)
{
        int viewport_rows = _history_viewport_rows(&termui->history, termui->rows);
        int active_rows_shown = termui->rows - viewport_rows;

        /* Send update if the cells are visible in viewport. */
        if (termui->update_cb && active_rows_shown > row)
                termui->update_cb(termui->update_udata, col, row + viewport_rows, &_screen_cell(termui, col, row), cells);
}

static void _update_current_cell(termui_t *termui)
{
        _update_active_cells(termui, termui->col, termui->row, 1);
}

static void _cursor_off(termui_t *termui)
{
        if (termui->cursor_visible) {
                _current_cell(termui).cursor = 0;
                _update_current_cell(termui);
        }
}

static void _cursor_on(termui_t *termui)
{
        if (termui->cursor_visible) {
                _current_cell(termui).cursor = 1;
                _update_current_cell(termui);
        }
}

static void _advance_line(termui_t *termui)
{
        if (termui->row + 1 >= termui->rows) {
                size_t old_top = termui->history.viewport_top;

                _termui_evict_row(termui);

                if (old_top != termui->history.viewport_top && termui->refresh_cb)
                        termui->refresh_cb(termui->refresh_udata);

                if (termui->scroll_cb && !_scrollback_active(&termui->history))
                        termui->scroll_cb(termui->scroll_udata, 1);
        }

        if (termui->rows > 1)
                termui->row++;

        if (termui->row >= termui->used_rows)
                termui->used_rows = termui->row + 1;

        assert(termui->row < termui->rows);
}

void termui_put_lf(termui_t *termui)
{
        _cursor_off(termui);
        termui->overflow = false;
        _advance_line(termui);
        _cursor_on(termui);
}

void termui_put_cr(termui_t *termui)
{
        _cursor_off(termui);

        /* CR right after overflow from previous row. */
        if (termui->overflow && termui->row > 0) {
                termui->row--;
                _overflow_flag(termui, termui->row) = 0;
        }

        termui->overflow = false;

        // Set position to start of current line.
        termui->col = 0;

        _cursor_on(termui);
}

/* Combined CR & LF to cut down on cursor update callbacks. */
void termui_put_crlf(termui_t *termui)
{
        _cursor_off(termui);

        /* CR right after overflow from previous row. */
        if (termui->overflow && termui->row > 0) {
                termui->row--;
                _overflow_flag(termui, termui->row) = 0;
        }

        termui->overflow = false;

        // Set position to start of next row.
        _advance_line(termui);
        termui->col = 0;

        _cursor_on(termui);
}

void termui_put_tab(termui_t *termui)
{
        _cursor_off(termui);

        termui->overflow = false;

        int new_col = (termui->col / 8 + 1) * 8;
        if (new_col >= termui->cols)
                new_col = termui->cols - 1;
        termui->col = new_col;

        _cursor_on(termui);
}

void termui_put_backspace(termui_t *termui)
{
        _cursor_off(termui);

        termui->overflow = false;

        if (termui->col == 0) {
                if (termui->row > 0 && _overflow_flag(termui, termui->row - 1)) {
                        termui->row--;
                        termui->col = termui->cols - 1;
                        _overflow_flag(termui, termui->row) = false;
                }
        } else {
                termui->col--;
        }

        _cursor_on(termui);
}

/**
 * Put glyph at current position, and advance column by width, overflowing into
 * next row and scrolling the active screen if necessary.
 *
 * If width > 1, the function makes sure the glyph isn't split by end of row.
 * The following (width - 1) cells are filled with padding cells,
 * and it's the user's responsibility to render this correctly.
 */
void termui_put_glyph(termui_t *termui, uint32_t glyph_idx, int width)
{
        if (termui->row >= termui->used_rows)
                termui->used_rows = termui->row + 1;

        termui_cell_t padding_cell = termui->style;
        padding_cell.padding = 1;
        termui_cell_t cell = termui->style;
        cell.glyph_idx = glyph_idx;

        // FIXME: handle wide glyphs in history correctly after resize

        if (termui->col + width > termui->cols) {
                /* Have to go to next row first. */
                int blanks = termui->cols - termui->col;
                for (int i = 0; i < blanks; i++)
                        _screen_cell(termui, termui->col + i, termui->row) = padding_cell;

                _update_active_cells(termui, termui->col, termui->row, blanks);

                _overflow_flag(termui, termui->row) = 1;
                _advance_line(termui);
                termui->col = 0;
        }

        _current_cell(termui) = cell;
        termui->col++;

        for (int i = 1; i < width; i++) {
                _current_cell(termui) = padding_cell;
                termui->col++;
        }

        if (termui->col < termui->cols) {
                /* The changed cells are all adjacent. */
                if (termui->cursor_visible)
                        _current_cell(termui).cursor = 1;
                _update_active_cells(termui, termui->col - width, termui->row, width + 1);
                termui->overflow = false;
        } else {
                /* Update the written cells and then update cursor on next row. */
                _update_active_cells(termui, termui->col - width, termui->row, width);

                _overflow_flag(termui, termui->row) = 1;
                _advance_line(termui);
                termui->col = 0;
                termui->overflow = true;

                _cursor_on(termui);
        }
}

termui_color_t termui_color_from_rgb(uint8_t r, uint8_t g, uint8_t b)
{
        r = r >> 3;
        g = g >> 3;
        b = b >> 3;

        return 0x8000 | r << 10 | g << 5 | b;
}

void termui_color_to_rgb(const termui_color_t c, uint8_t *r, uint8_t *g, uint8_t *b)
{
        assert((c & 0x8000) != 0);

        /* 15b encoding, bit 15 is set to reserve lower half for other uses. */

        int bb = c & 0x1f;
        int gg = (c >> 5) & 0x1f;
        int rr = (c >> 10) & 0x1f;

        /*
         * 3 extra low order bits are filled from high-order bits to get the full
         * range instead of topping out at 0xf8.
         */
        *r = (rr << 3) | (rr >> 2);
        *g = (gg << 3) | (gg >> 2);
        *b = (bb << 3) | (bb >> 2);

        assert(termui_color_from_rgb(*r, *g, *b) == c);
}

/** Get terminal width.
 */
int termui_get_cols(const termui_t *termui)
{
        return termui->cols;
}

/** Get terminal height.
 */
int termui_get_rows(const termui_t *termui)
{
        return termui->rows;
}

/** Get cursor position
 */
void termui_get_pos(const termui_t *termui, int *col, int *row)
{
        *col = termui->col;
        *row = termui->row;
}

/** Set cursor position.
 */
void termui_set_pos(termui_t *termui, int col, int row)
{
        if (col < 0)
                col = 0;

        if (col >= termui->cols)
                col = termui->cols - 1;

        if (row < 0)
                row = 0;

        if (row >= termui->rows)
                row = termui->rows - 1;

        _cursor_off(termui);

        termui->col = col;
        termui->row = row;

        _cursor_on(termui);
}

/** Clear screen by scrolling out all text currently on screen.
 * Sets position to (0, 0).
 */
void termui_clear_screen(termui_t *termui)
{
        _cursor_off(termui);
        termui_put_crlf(termui);

        int unused_rows = termui->rows - termui->used_rows;

        while (termui->used_rows > 0)
                _termui_evict_row(termui);

        /* Clear out potential garbage left by direct screen access. */
        for (int row = 0; row < unused_rows; row++) {
                for (int col = 0; col < termui->cols; col++) {
                        _screen_cell(termui, col, row) = termui->default_cell;
                }
        }

        termui->row = 0;
        termui->col = 0;

        _cursor_on(termui);

        if (termui->refresh_cb)
                termui->refresh_cb(termui->refresh_udata);
}

/** Erase all text starting at the given row.
 * Erased text is not appended to history.
 * If cursor was in the erased section, it's set to the beginning of it.
 */
void termui_wipe_screen(termui_t *termui, int first_row)
{
        if (first_row >= termui->rows)
                return;

        if (first_row < 0)
                first_row = 0;

        for (int row = first_row; row < termui->rows; row++) {
                for (int col = 0; col < termui->cols; col++)
                        _screen_cell(termui, col, row) = termui->default_cell;

                _overflow_flag(termui, row) = false;
                _update_active_cells(termui, 0, row, termui->cols);
        }

        if (termui->used_rows > first_row)
                termui->used_rows = first_row;

        if (termui->row >= first_row) {
                termui->row = first_row;
                termui->col = 0;
                _cursor_on(termui);
        }
}

void termui_set_scroll_cb(termui_t *termui, termui_scroll_cb_t cb, void *userdata)
{
        termui->scroll_cb = cb;
        termui->scroll_udata = userdata;
}

void termui_set_update_cb(termui_t *termui, termui_update_cb_t cb, void *userdata)
{
        termui->update_cb = cb;
        termui->update_udata = userdata;
}

void termui_set_refresh_cb(termui_t *termui, termui_refresh_cb_t cb, void *userdata)
{
        termui->refresh_cb = cb;
        termui->refresh_udata = userdata;
}

/** Makes update callbacks for all indicated viewport rows.
 * Useful when refreshing the screens or handling a scroll callback.
 */
void termui_force_viewport_update(const termui_t *termui, int first_row, int rows)
{
        assert(first_row >= 0);
        assert(rows >= 0);
        assert(first_row + rows <= termui->rows);

        if (!termui->update_cb)
                return;

        int sb_rows = _history_viewport_rows(&termui->history, termui->rows);
        int updated = _history_iter_rows(&termui->history, first_row, rows, termui->update_cb, termui->update_udata);

        first_row += updated;
        rows -= updated;

        assert(sb_rows <= first_row);

        for (int row = first_row; row < first_row + rows; row++) {
                termui->update_cb(termui->update_udata, 0, row, &_screen_cell(termui, 0, row - sb_rows), termui->cols);
        }
}

bool termui_scrollback_is_active(const termui_t *termui)
{
        return _scrollback_active(&termui->history);
}

termui_t *termui_create(int cols, int rows, size_t history_lines)
{
        /* Prevent numerical overflows. */
        if (cols < 2 || rows < 1 || INT_MAX / cols < rows)
                return NULL;

        int cells = cols * rows;

        termui_t *termui = calloc(1, sizeof(termui_t));
        if (!termui)
                return NULL;

        termui->cols = cols;
        termui->rows = rows;
        termui->history.lines.max_len = history_lines;
        if (history_lines > SIZE_MAX / cols)
                termui->history.cells.max_len = SIZE_MAX;
        else
                termui->history.cells.max_len = history_lines * cols;
        termui->history.cols = cols;

        termui->screen = calloc(cells, sizeof(termui->screen[0]));
        if (!termui->screen) {
                free(termui);
                return NULL;
        }

        termui->overflow_flags = calloc(rows, sizeof(termui->overflow_flags[0]));
        if (!termui->overflow_flags) {
                free(termui->screen);
                free(termui);
                return NULL;
        }

        return termui;
}

void termui_destroy(termui_t *termui)
{
        free(termui->screen);
        free(termui);
}

/** Scrolls the viewport.
 * Negative delta scrolls towards older rows, positive towards newer.
 * Scroll callback is called with the actual number of rows scrolled.
 * No callback is called for rows previously off-screen.
 *
 * @param termui
 * @param delta  Number of rows to scroll.
 */
void termui_history_scroll(termui_t *termui, int delta)
{
        int scrolled = _history_scroll(&termui->history, delta);

        if (scrolled != 0 && termui->scroll_cb)
                termui->scroll_cb(termui->scroll_udata, scrolled);
}

void termui_set_cursor_visibility(termui_t *termui, bool visible)
{
        if (termui->cursor_visible == visible)
                return;

        termui->cursor_visible = visible;

        _current_cell(termui).cursor = visible;
        _update_current_cell(termui);
}

bool termui_get_cursor_visibility(const termui_t *termui)
{
        return termui->cursor_visible;
}

static void _termui_put_cells(termui_t *termui, const termui_cell_t *cells, int n)
{
        while (n > 0) {
                _current_cell(termui) = cells[0];
                cells++;
                n--;

                termui->col++;

                if (termui->col == termui->cols) {
                        _overflow_flag(termui, termui->row) = 1;
                        _advance_line(termui);
                        termui->col = 0;
                        termui->overflow = true;
                } else {
                        termui->overflow = false;
                }
        }

        if (termui->row >= termui->used_rows)
                termui->used_rows = termui->row + 1;
}

/** Resize active screen and scrollback depth.
 */
errno_t termui_resize(termui_t *termui, int cols, int rows, size_t history_lines)
{
        /* Prevent numerical overflows. */
        if (cols < 2 || rows < 1 || INT_MAX / cols < rows)
                return ERANGE;

        int cells = cols * rows;

        termui_cell_t *new_screen = calloc(cells, sizeof(new_screen[0]));
        if (!new_screen)
                return ENOMEM;

        uint8_t *new_flags = calloc(rows, sizeof(new_flags[0]));
        if (!new_flags) {
                free(new_screen);
                return ENOMEM;
        }

        termui_t old_termui = *termui;

        termui->rows = rows;
        termui->cols = cols;
        termui->row = 0;
        termui->col = 0;
        termui->used_rows = 0;
        termui->first_row = 0;
        termui->screen = new_screen;
        termui->overflow_flags = new_flags;
        termui->overflow = false;

        bool cursor_visible = termui->cursor_visible;
        termui->cursor_visible = false;

        termui->history.lines.max_len = history_lines;

        if (history_lines > SIZE_MAX / cols)
                termui->history.cells.max_len = SIZE_MAX;
        else
                termui->history.cells.max_len = history_lines * cols;

        /* Temporarily remove callbacks. */
        termui->scroll_cb = NULL;
        termui->update_cb = NULL;
        termui->refresh_cb = NULL;

        size_t recouped;
        const termui_cell_t *c = _history_reflow(&termui->history, cols, &recouped);

        /* Return piece of the incomplete line in scrollback back to active screen. */
        if (recouped > 0)
                _termui_put_cells(termui, c, recouped);

        /* Mark cursor position. */
        _current_cell(&old_termui).cursor = 1;

        /* Write the contents of old screen into the new one. */
        for (int row = 0; row < old_termui.used_rows; row++) {
                int real_row_offset = _real_row(&old_termui, row) * old_termui.cols;

                if (_overflow_flag(&old_termui, row)) {
                        _termui_put_cells(termui, &old_termui.screen[real_row_offset], old_termui.cols);
                } else {
                        /* Trim trailing blanks. */
                        int len = old_termui.cols;
                        while (len > 0 && _cell_is_empty(old_termui.screen[real_row_offset + len - 1]))
                                len--;

                        _termui_put_cells(termui, &old_termui.screen[real_row_offset], len);

                        /* Mark cursor at the end of row, if any. */
                        if (len < old_termui.cols)
                                _current_cell(termui).cursor = old_termui.screen[real_row_offset + len].cursor;

                        if (row < old_termui.used_rows - 1)
                                termui_put_crlf(termui);
                }
        }

        /* Find cursor */
        int new_col = 0;
        int new_row = 0;
        for (int col = 0; col < termui->cols; col++) {
                for (int row = 0; row < termui->rows; row++) {
                        if (_screen_cell(termui, col, row).cursor) {
                                _screen_cell(termui, col, row).cursor = 0;
                                new_col = col;
                                new_row = row;
                        }
                }
        }

        free(old_termui.screen);
        free(old_termui.overflow_flags);

        termui->col = new_col;
        termui->row = new_row;

        termui->cursor_visible = cursor_visible;
        _cursor_on(termui);

        termui->scroll_cb = old_termui.scroll_cb;
        termui->update_cb = old_termui.update_cb;
        termui->refresh_cb = old_termui.refresh_cb;

        if (termui->refresh_cb)
                termui->refresh_cb(termui->refresh_udata);

        return EOK;
}

/* [<][>][^][v][top][bottom][index][help] */
HelenOS homepage, sources at GitHub