[899bdfd] | 1 | /*
|
---|
| 2 | * Copyright (c) 2024 Jiří Zárevúcky
|
---|
| 3 | * All rights reserved.
|
---|
| 4 | *
|
---|
| 5 | * Redistribution and use in source and binary forms, with or without
|
---|
| 6 | * modification, are permitted provided that the following conditions
|
---|
| 7 | * are met:
|
---|
| 8 | *
|
---|
| 9 | * - Redistributions of source code must retain the above copyright
|
---|
| 10 | * notice, this list of conditions and the following disclaimer.
|
---|
| 11 | * - Redistributions in binary form must reproduce the above copyright
|
---|
| 12 | * notice, this list of conditions and the following disclaimer in the
|
---|
| 13 | * documentation and/or other materials provided with the distribution.
|
---|
| 14 | * - The name of the author may not be used to endorse or promote products
|
---|
| 15 | * derived from this software without specific prior written permission.
|
---|
| 16 | *
|
---|
| 17 | * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
---|
| 18 | * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
---|
| 19 | * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
---|
| 20 | * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
---|
| 21 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
---|
| 22 | * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
---|
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
---|
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
---|
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
---|
| 26 | * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
---|
| 27 | */
|
---|
| 28 |
|
---|
| 29 | #include <termui.h>
|
---|
| 30 |
|
---|
| 31 | #include <assert.h>
|
---|
| 32 | #include <limits.h>
|
---|
| 33 | #include <stdio.h>
|
---|
| 34 | #include <stdlib.h>
|
---|
| 35 |
|
---|
| 36 | #include "history.h"
|
---|
| 37 |
|
---|
| 38 | struct termui {
|
---|
| 39 | int cols;
|
---|
| 40 | int rows;
|
---|
| 41 |
|
---|
| 42 | int col;
|
---|
| 43 | int row;
|
---|
| 44 |
|
---|
| 45 | bool cursor_visible;
|
---|
| 46 |
|
---|
| 47 | // How much of the screen is in use. Relevant for clrscr.
|
---|
| 48 | int used_rows;
|
---|
| 49 |
|
---|
| 50 | // Row index of the first screen row in the circular screen buffer.
|
---|
| 51 | int first_row;
|
---|
| 52 | // rows * cols circular buffer of the current virtual screen contents.
|
---|
| 53 | // Does not necessarily correspond to the currently visible text,
|
---|
| 54 | // if scrollback is active.
|
---|
| 55 | termui_cell_t *screen;
|
---|
| 56 | // Set to one if the corresponding row has overflowed into the next row.
|
---|
| 57 | uint8_t *overflow_flags;
|
---|
| 58 |
|
---|
| 59 | /* Used to remove extra newline when CRLF is placed exactly on row boundary. */
|
---|
| 60 | bool overflow;
|
---|
| 61 |
|
---|
| 62 | struct history history;
|
---|
| 63 |
|
---|
| 64 | termui_cell_t style;
|
---|
| 65 | termui_cell_t default_cell;
|
---|
| 66 |
|
---|
| 67 | termui_scroll_cb_t scroll_cb;
|
---|
| 68 | termui_update_cb_t update_cb;
|
---|
| 69 | termui_refresh_cb_t refresh_cb;
|
---|
| 70 | void *scroll_udata;
|
---|
| 71 | void *update_udata;
|
---|
| 72 | void *refresh_udata;
|
---|
| 73 | };
|
---|
| 74 |
|
---|
| 75 | static int _real_row(const termui_t *termui, int row)
|
---|
| 76 | {
|
---|
| 77 | row += termui->first_row;
|
---|
| 78 | if (row >= termui->rows)
|
---|
| 79 | row -= termui->rows;
|
---|
| 80 |
|
---|
| 81 | return row;
|
---|
| 82 | }
|
---|
| 83 |
|
---|
| 84 | #define _screen_cell(termui, col, row) \
|
---|
| 85 | ((termui)->screen[(termui)->cols * _real_row((termui), (row)) + (col)])
|
---|
| 86 |
|
---|
| 87 | #define _current_cell(termui) \
|
---|
| 88 | _screen_cell((termui), (termui)->col, (termui)->row)
|
---|
| 89 |
|
---|
| 90 | #define _overflow_flag(termui, row) \
|
---|
| 91 | ((termui)->overflow_flags[_real_row((termui), (row))])
|
---|
| 92 |
|
---|
| 93 | /** Sets current cell style/color.
|
---|
| 94 | */
|
---|
| 95 | void termui_set_style(termui_t *termui, termui_cell_t style)
|
---|
| 96 | {
|
---|
| 97 | termui->style = style;
|
---|
| 98 | }
|
---|
| 99 |
|
---|
| 100 | static void _termui_evict_row(termui_t *termui)
|
---|
| 101 | {
|
---|
| 102 | if (termui->used_rows <= 0)
|
---|
| 103 | return;
|
---|
| 104 |
|
---|
| 105 | bool last = !_overflow_flag(termui, 0);
|
---|
| 106 |
|
---|
| 107 | for (int col = 0; col < termui->cols; col++)
|
---|
| 108 | _screen_cell(termui, col, 0).cursor = 0;
|
---|
| 109 |
|
---|
| 110 | /* Append first row of the screen to history. */
|
---|
| 111 | _history_append_row(&termui->history, &_screen_cell(termui, 0, 0), last);
|
---|
| 112 |
|
---|
| 113 | _overflow_flag(termui, 0) = false;
|
---|
| 114 |
|
---|
| 115 | /* Clear the row we moved to history. */
|
---|
| 116 | for (int col = 0; col < termui->cols; col++)
|
---|
| 117 | _screen_cell(termui, col, 0) = termui->default_cell;
|
---|
| 118 |
|
---|
| 119 | termui->used_rows--;
|
---|
| 120 |
|
---|
| 121 | termui->row--;
|
---|
| 122 | if (termui->row < 0) {
|
---|
| 123 | termui->row = 0;
|
---|
| 124 | termui->col = 0;
|
---|
| 125 | }
|
---|
| 126 |
|
---|
| 127 | termui->first_row++;
|
---|
| 128 | if (termui->first_row >= termui->rows)
|
---|
| 129 | termui->first_row -= termui->rows;
|
---|
| 130 |
|
---|
| 131 | assert(termui->first_row < termui->rows);
|
---|
| 132 | }
|
---|
| 133 |
|
---|
| 134 | /**
|
---|
| 135 | * Get active screen row. This always points to the primary output buffer,
|
---|
| 136 | * unaffected by viewport shifting. Can be used for modifying the screen
|
---|
| 137 | * directly. For displaying viewport, use termui_force_viewport_update().
|
---|
| 138 | */
|
---|
| 139 | termui_cell_t *termui_get_active_row(termui_t *termui, int row)
|
---|
| 140 | {
|
---|
| 141 | assert(row >= 0);
|
---|
| 142 | assert(row < termui->rows);
|
---|
| 143 |
|
---|
| 144 | return &_screen_cell(termui, 0, row);
|
---|
| 145 | }
|
---|
| 146 |
|
---|
| 147 | static void _update_active_cells(termui_t *termui, int col, int row, int cells)
|
---|
| 148 | {
|
---|
| 149 | int viewport_rows = _history_viewport_rows(&termui->history, termui->rows);
|
---|
| 150 | int active_rows_shown = termui->rows - viewport_rows;
|
---|
| 151 |
|
---|
| 152 | /* Send update if the cells are visible in viewport. */
|
---|
| 153 | if (termui->update_cb && active_rows_shown > row)
|
---|
| 154 | termui->update_cb(termui->update_udata, col, row + viewport_rows, &_screen_cell(termui, col, row), cells);
|
---|
| 155 | }
|
---|
| 156 |
|
---|
| 157 | static void _update_current_cell(termui_t *termui)
|
---|
| 158 | {
|
---|
| 159 | _update_active_cells(termui, termui->col, termui->row, 1);
|
---|
| 160 | }
|
---|
| 161 |
|
---|
| 162 | static void _cursor_off(termui_t *termui)
|
---|
| 163 | {
|
---|
| 164 | if (termui->cursor_visible) {
|
---|
| 165 | _current_cell(termui).cursor = 0;
|
---|
| 166 | _update_current_cell(termui);
|
---|
| 167 | }
|
---|
| 168 | }
|
---|
| 169 |
|
---|
| 170 | static void _cursor_on(termui_t *termui)
|
---|
| 171 | {
|
---|
| 172 | if (termui->cursor_visible) {
|
---|
| 173 | _current_cell(termui).cursor = 1;
|
---|
| 174 | _update_current_cell(termui);
|
---|
| 175 | }
|
---|
| 176 | }
|
---|
| 177 |
|
---|
| 178 | static void _advance_line(termui_t *termui)
|
---|
| 179 | {
|
---|
| 180 | if (termui->row + 1 >= termui->rows) {
|
---|
| 181 | size_t old_top = termui->history.viewport_top;
|
---|
| 182 |
|
---|
| 183 | _termui_evict_row(termui);
|
---|
| 184 |
|
---|
| 185 | if (old_top != termui->history.viewport_top && termui->refresh_cb)
|
---|
| 186 | termui->refresh_cb(termui->refresh_udata);
|
---|
| 187 |
|
---|
| 188 | if (termui->scroll_cb && !_scrollback_active(&termui->history))
|
---|
| 189 | termui->scroll_cb(termui->scroll_udata, 1);
|
---|
| 190 | }
|
---|
| 191 |
|
---|
[2cf8f994] | 192 | if (termui->rows > 1)
|
---|
| 193 | termui->row++;
|
---|
[899bdfd] | 194 |
|
---|
| 195 | if (termui->row >= termui->used_rows)
|
---|
| 196 | termui->used_rows = termui->row + 1;
|
---|
| 197 |
|
---|
| 198 | assert(termui->row < termui->rows);
|
---|
| 199 | }
|
---|
| 200 |
|
---|
| 201 | void termui_put_lf(termui_t *termui)
|
---|
| 202 | {
|
---|
| 203 | _cursor_off(termui);
|
---|
| 204 | termui->overflow = false;
|
---|
| 205 | _advance_line(termui);
|
---|
| 206 | _cursor_on(termui);
|
---|
| 207 | }
|
---|
| 208 |
|
---|
| 209 | void termui_put_cr(termui_t *termui)
|
---|
| 210 | {
|
---|
| 211 | _cursor_off(termui);
|
---|
| 212 |
|
---|
| 213 | /* CR right after overflow from previous row. */
|
---|
| 214 | if (termui->overflow && termui->row > 0) {
|
---|
| 215 | termui->row--;
|
---|
| 216 | _overflow_flag(termui, termui->row) = 0;
|
---|
| 217 | }
|
---|
| 218 |
|
---|
| 219 | termui->overflow = false;
|
---|
| 220 |
|
---|
| 221 | // Set position to start of current line.
|
---|
| 222 | termui->col = 0;
|
---|
| 223 |
|
---|
| 224 | _cursor_on(termui);
|
---|
| 225 | }
|
---|
| 226 |
|
---|
| 227 | /* Combined CR & LF to cut down on cursor update callbacks. */
|
---|
| 228 | void termui_put_crlf(termui_t *termui)
|
---|
| 229 | {
|
---|
| 230 | _cursor_off(termui);
|
---|
| 231 |
|
---|
| 232 | /* CR right after overflow from previous row. */
|
---|
| 233 | if (termui->overflow && termui->row > 0) {
|
---|
| 234 | termui->row--;
|
---|
| 235 | _overflow_flag(termui, termui->row) = 0;
|
---|
| 236 | }
|
---|
| 237 |
|
---|
| 238 | termui->overflow = false;
|
---|
| 239 |
|
---|
| 240 | // Set position to start of next row.
|
---|
| 241 | _advance_line(termui);
|
---|
| 242 | termui->col = 0;
|
---|
| 243 |
|
---|
| 244 | _cursor_on(termui);
|
---|
| 245 | }
|
---|
| 246 |
|
---|
| 247 | void termui_put_tab(termui_t *termui)
|
---|
| 248 | {
|
---|
| 249 | _cursor_off(termui);
|
---|
| 250 |
|
---|
| 251 | termui->overflow = false;
|
---|
| 252 |
|
---|
| 253 | int new_col = (termui->col / 8 + 1) * 8;
|
---|
| 254 | if (new_col >= termui->cols)
|
---|
| 255 | new_col = termui->cols - 1;
|
---|
| 256 | termui->col = new_col;
|
---|
| 257 |
|
---|
| 258 | _cursor_on(termui);
|
---|
| 259 | }
|
---|
| 260 |
|
---|
| 261 | void termui_put_backspace(termui_t *termui)
|
---|
| 262 | {
|
---|
| 263 | _cursor_off(termui);
|
---|
| 264 |
|
---|
| 265 | termui->overflow = false;
|
---|
| 266 |
|
---|
| 267 | if (termui->col == 0) {
|
---|
| 268 | if (termui->row > 0 && _overflow_flag(termui, termui->row - 1)) {
|
---|
| 269 | termui->row--;
|
---|
| 270 | termui->col = termui->cols - 1;
|
---|
| 271 | _overflow_flag(termui, termui->row) = false;
|
---|
| 272 | }
|
---|
| 273 | } else {
|
---|
| 274 | termui->col--;
|
---|
| 275 | }
|
---|
| 276 |
|
---|
| 277 | _cursor_on(termui);
|
---|
| 278 | }
|
---|
| 279 |
|
---|
| 280 | /**
|
---|
| 281 | * Put glyph at current position, and advance column by width, overflowing into
|
---|
| 282 | * next row and scrolling the active screen if necessary.
|
---|
| 283 | *
|
---|
| 284 | * If width > 1, the function makes sure the glyph isn't split by end of row.
|
---|
| 285 | * The following (width - 1) cells are filled with padding cells,
|
---|
| 286 | * and it's the user's responsibility to render this correctly.
|
---|
| 287 | */
|
---|
| 288 | void termui_put_glyph(termui_t *termui, uint32_t glyph_idx, int width)
|
---|
| 289 | {
|
---|
| 290 | if (termui->row >= termui->used_rows)
|
---|
| 291 | termui->used_rows = termui->row + 1;
|
---|
| 292 |
|
---|
| 293 | termui_cell_t padding_cell = termui->style;
|
---|
| 294 | padding_cell.padding = 1;
|
---|
| 295 | termui_cell_t cell = termui->style;
|
---|
| 296 | cell.glyph_idx = glyph_idx;
|
---|
| 297 |
|
---|
| 298 | // FIXME: handle wide glyphs in history correctly after resize
|
---|
| 299 |
|
---|
| 300 | if (termui->col + width > termui->cols) {
|
---|
| 301 | /* Have to go to next row first. */
|
---|
| 302 | int blanks = termui->cols - termui->col;
|
---|
| 303 | for (int i = 0; i < blanks; i++)
|
---|
| 304 | _screen_cell(termui, termui->col + i, termui->row) = padding_cell;
|
---|
| 305 |
|
---|
| 306 | _update_active_cells(termui, termui->col, termui->row, blanks);
|
---|
| 307 |
|
---|
| 308 | _overflow_flag(termui, termui->row) = 1;
|
---|
| 309 | _advance_line(termui);
|
---|
| 310 | termui->col = 0;
|
---|
| 311 | }
|
---|
| 312 |
|
---|
| 313 | _current_cell(termui) = cell;
|
---|
| 314 | termui->col++;
|
---|
| 315 |
|
---|
| 316 | for (int i = 1; i < width; i++) {
|
---|
| 317 | _current_cell(termui) = padding_cell;
|
---|
| 318 | termui->col++;
|
---|
| 319 | }
|
---|
| 320 |
|
---|
| 321 | if (termui->col < termui->cols) {
|
---|
| 322 | /* The changed cells are all adjacent. */
|
---|
| 323 | if (termui->cursor_visible)
|
---|
| 324 | _current_cell(termui).cursor = 1;
|
---|
| 325 | _update_active_cells(termui, termui->col - width, termui->row, width + 1);
|
---|
| 326 | termui->overflow = false;
|
---|
| 327 | } else {
|
---|
| 328 | /* Update the written cells and then update cursor on next row. */
|
---|
| 329 | _update_active_cells(termui, termui->col - width, termui->row, width);
|
---|
| 330 |
|
---|
| 331 | _overflow_flag(termui, termui->row) = 1;
|
---|
| 332 | _advance_line(termui);
|
---|
| 333 | termui->col = 0;
|
---|
| 334 | termui->overflow = true;
|
---|
| 335 |
|
---|
| 336 | _cursor_on(termui);
|
---|
| 337 | }
|
---|
| 338 | }
|
---|
| 339 |
|
---|
| 340 | termui_color_t termui_color_from_rgb(uint8_t r, uint8_t g, uint8_t b)
|
---|
| 341 | {
|
---|
| 342 | r = r >> 3;
|
---|
| 343 | g = g >> 3;
|
---|
| 344 | b = b >> 3;
|
---|
| 345 |
|
---|
| 346 | return 0x8000 | r << 10 | g << 5 | b;
|
---|
| 347 | }
|
---|
| 348 |
|
---|
| 349 | void termui_color_to_rgb(const termui_color_t c, uint8_t *r, uint8_t *g, uint8_t *b)
|
---|
| 350 | {
|
---|
| 351 | assert((c & 0x8000) != 0);
|
---|
| 352 |
|
---|
| 353 | /* 15b encoding, bit 15 is set to reserve lower half for other uses. */
|
---|
| 354 |
|
---|
| 355 | int bb = c & 0x1f;
|
---|
| 356 | int gg = (c >> 5) & 0x1f;
|
---|
| 357 | int rr = (c >> 10) & 0x1f;
|
---|
| 358 |
|
---|
| 359 | /*
|
---|
| 360 | * 3 extra low order bits are filled from high-order bits to get the full
|
---|
| 361 | * range instead of topping out at 0xf8.
|
---|
| 362 | */
|
---|
| 363 | *r = (rr << 3) | (rr >> 2);
|
---|
| 364 | *g = (gg << 3) | (gg >> 2);
|
---|
| 365 | *b = (bb << 3) | (bb >> 2);
|
---|
| 366 |
|
---|
| 367 | assert(termui_color_from_rgb(*r, *g, *b) == c);
|
---|
| 368 | }
|
---|
| 369 |
|
---|
| 370 | /** Get terminal width.
|
---|
| 371 | */
|
---|
| 372 | int termui_get_cols(const termui_t *termui)
|
---|
| 373 | {
|
---|
| 374 | return termui->cols;
|
---|
| 375 | }
|
---|
| 376 |
|
---|
| 377 | /** Get terminal height.
|
---|
| 378 | */
|
---|
| 379 | int termui_get_rows(const termui_t *termui)
|
---|
| 380 | {
|
---|
| 381 | return termui->rows;
|
---|
| 382 | }
|
---|
| 383 |
|
---|
| 384 | /** Get cursor position
|
---|
| 385 | */
|
---|
| 386 | void termui_get_pos(const termui_t *termui, int *col, int *row)
|
---|
| 387 | {
|
---|
| 388 | *col = termui->col;
|
---|
| 389 | *row = termui->row;
|
---|
| 390 | }
|
---|
| 391 |
|
---|
| 392 | /** Set cursor position.
|
---|
| 393 | */
|
---|
| 394 | void termui_set_pos(termui_t *termui, int col, int row)
|
---|
| 395 | {
|
---|
| 396 | if (col < 0)
|
---|
| 397 | col = 0;
|
---|
| 398 |
|
---|
| 399 | if (col >= termui->cols)
|
---|
| 400 | col = termui->cols - 1;
|
---|
| 401 |
|
---|
| 402 | if (row < 0)
|
---|
| 403 | row = 0;
|
---|
| 404 |
|
---|
| 405 | if (row >= termui->rows)
|
---|
| 406 | row = termui->rows - 1;
|
---|
| 407 |
|
---|
| 408 | _cursor_off(termui);
|
---|
| 409 |
|
---|
| 410 | termui->col = col;
|
---|
| 411 | termui->row = row;
|
---|
| 412 |
|
---|
| 413 | _cursor_on(termui);
|
---|
| 414 | }
|
---|
| 415 |
|
---|
| 416 | /** Clear screen by scrolling out all text currently on screen.
|
---|
| 417 | * Sets position to (0, 0).
|
---|
| 418 | */
|
---|
| 419 | void termui_clear_screen(termui_t *termui)
|
---|
| 420 | {
|
---|
| 421 | _cursor_off(termui);
|
---|
| 422 | termui_put_crlf(termui);
|
---|
| 423 |
|
---|
| 424 | int unused_rows = termui->rows - termui->used_rows;
|
---|
| 425 |
|
---|
| 426 | while (termui->used_rows > 0)
|
---|
| 427 | _termui_evict_row(termui);
|
---|
| 428 |
|
---|
| 429 | /* Clear out potential garbage left by direct screen access. */
|
---|
| 430 | for (int row = 0; row < unused_rows; row++) {
|
---|
| 431 | for (int col = 0; col < termui->cols; col++) {
|
---|
| 432 | _screen_cell(termui, col, row) = termui->default_cell;
|
---|
| 433 | }
|
---|
| 434 | }
|
---|
| 435 |
|
---|
| 436 | termui->row = 0;
|
---|
| 437 | termui->col = 0;
|
---|
| 438 |
|
---|
| 439 | _cursor_on(termui);
|
---|
| 440 |
|
---|
| 441 | if (termui->refresh_cb)
|
---|
| 442 | termui->refresh_cb(termui->refresh_udata);
|
---|
| 443 | }
|
---|
| 444 |
|
---|
| 445 | /** Erase all text starting at the given row.
|
---|
| 446 | * Erased text is not appended to history.
|
---|
| 447 | * If cursor was in the erased section, it's set to the beginning of it.
|
---|
| 448 | */
|
---|
| 449 | void termui_wipe_screen(termui_t *termui, int first_row)
|
---|
| 450 | {
|
---|
| 451 | if (first_row >= termui->rows)
|
---|
| 452 | return;
|
---|
| 453 |
|
---|
| 454 | if (first_row < 0)
|
---|
| 455 | first_row = 0;
|
---|
| 456 |
|
---|
| 457 | for (int row = first_row; row < termui->rows; row++) {
|
---|
| 458 | for (int col = 0; col < termui->cols; col++)
|
---|
| 459 | _screen_cell(termui, col, row) = termui->default_cell;
|
---|
| 460 |
|
---|
[84cc190] | 461 | _overflow_flag(termui, row) = false;
|
---|
[899bdfd] | 462 | _update_active_cells(termui, 0, row, termui->cols);
|
---|
| 463 | }
|
---|
| 464 |
|
---|
| 465 | if (termui->used_rows > first_row)
|
---|
| 466 | termui->used_rows = first_row;
|
---|
| 467 |
|
---|
| 468 | if (termui->row >= first_row) {
|
---|
| 469 | termui->row = first_row;
|
---|
| 470 | termui->col = 0;
|
---|
| 471 | _cursor_on(termui);
|
---|
| 472 | }
|
---|
| 473 | }
|
---|
| 474 |
|
---|
| 475 | void termui_set_scroll_cb(termui_t *termui, termui_scroll_cb_t cb, void *userdata)
|
---|
| 476 | {
|
---|
| 477 | termui->scroll_cb = cb;
|
---|
| 478 | termui->scroll_udata = userdata;
|
---|
| 479 | }
|
---|
| 480 |
|
---|
| 481 | void termui_set_update_cb(termui_t *termui, termui_update_cb_t cb, void *userdata)
|
---|
| 482 | {
|
---|
| 483 | termui->update_cb = cb;
|
---|
| 484 | termui->update_udata = userdata;
|
---|
| 485 | }
|
---|
| 486 |
|
---|
| 487 | void termui_set_refresh_cb(termui_t *termui, termui_refresh_cb_t cb, void *userdata)
|
---|
| 488 | {
|
---|
| 489 | termui->refresh_cb = cb;
|
---|
| 490 | termui->refresh_udata = userdata;
|
---|
| 491 | }
|
---|
| 492 |
|
---|
| 493 | /** Makes update callbacks for all indicated viewport rows.
|
---|
| 494 | * Useful when refreshing the screens or handling a scroll callback.
|
---|
| 495 | */
|
---|
| 496 | void termui_force_viewport_update(const termui_t *termui, int first_row, int rows)
|
---|
| 497 | {
|
---|
| 498 | assert(first_row >= 0);
|
---|
| 499 | assert(rows >= 0);
|
---|
| 500 | assert(first_row + rows <= termui->rows);
|
---|
| 501 |
|
---|
| 502 | if (!termui->update_cb)
|
---|
| 503 | return;
|
---|
| 504 |
|
---|
| 505 | int sb_rows = _history_viewport_rows(&termui->history, termui->rows);
|
---|
| 506 | int updated = _history_iter_rows(&termui->history, first_row, rows, termui->update_cb, termui->update_udata);
|
---|
| 507 |
|
---|
| 508 | first_row += updated;
|
---|
| 509 | rows -= updated;
|
---|
| 510 |
|
---|
| 511 | assert(sb_rows <= first_row);
|
---|
| 512 |
|
---|
| 513 | for (int row = first_row; row < first_row + rows; row++) {
|
---|
| 514 | termui->update_cb(termui->update_udata, 0, row, &_screen_cell(termui, 0, row - sb_rows), termui->cols);
|
---|
| 515 | }
|
---|
| 516 | }
|
---|
| 517 |
|
---|
| 518 | bool termui_scrollback_is_active(const termui_t *termui)
|
---|
| 519 | {
|
---|
| 520 | return _scrollback_active(&termui->history);
|
---|
| 521 | }
|
---|
| 522 |
|
---|
| 523 | termui_t *termui_create(int cols, int rows, size_t history_lines)
|
---|
| 524 | {
|
---|
| 525 | /* Prevent numerical overflows. */
|
---|
| 526 | if (cols < 2 || rows < 1 || INT_MAX / cols < rows)
|
---|
| 527 | return NULL;
|
---|
| 528 |
|
---|
| 529 | int cells = cols * rows;
|
---|
| 530 |
|
---|
| 531 | termui_t *termui = calloc(1, sizeof(termui_t));
|
---|
| 532 | if (!termui)
|
---|
| 533 | return NULL;
|
---|
| 534 |
|
---|
| 535 | termui->cols = cols;
|
---|
| 536 | termui->rows = rows;
|
---|
| 537 | termui->history.lines.max_len = history_lines;
|
---|
| 538 | if (history_lines > SIZE_MAX / cols)
|
---|
| 539 | termui->history.cells.max_len = SIZE_MAX;
|
---|
| 540 | else
|
---|
| 541 | termui->history.cells.max_len = history_lines * cols;
|
---|
| 542 | termui->history.cols = cols;
|
---|
| 543 |
|
---|
| 544 | termui->screen = calloc(cells, sizeof(termui->screen[0]));
|
---|
| 545 | if (!termui->screen) {
|
---|
| 546 | free(termui);
|
---|
| 547 | return NULL;
|
---|
| 548 | }
|
---|
| 549 |
|
---|
| 550 | termui->overflow_flags = calloc(rows, sizeof(termui->overflow_flags[0]));
|
---|
| 551 | if (!termui->overflow_flags) {
|
---|
| 552 | free(termui->screen);
|
---|
| 553 | free(termui);
|
---|
| 554 | return NULL;
|
---|
| 555 | }
|
---|
| 556 |
|
---|
| 557 | return termui;
|
---|
| 558 | }
|
---|
| 559 |
|
---|
| 560 | void termui_destroy(termui_t *termui)
|
---|
| 561 | {
|
---|
| 562 | free(termui->screen);
|
---|
| 563 | free(termui);
|
---|
| 564 | }
|
---|
| 565 |
|
---|
| 566 | /** Scrolls the viewport.
|
---|
| 567 | * Negative delta scrolls towards older rows, positive towards newer.
|
---|
| 568 | * Scroll callback is called with the actual number of rows scrolled.
|
---|
| 569 | * No callback is called for rows previously off-screen.
|
---|
| 570 | *
|
---|
| 571 | * @param termui
|
---|
| 572 | * @param delta Number of rows to scroll.
|
---|
| 573 | */
|
---|
| 574 | void termui_history_scroll(termui_t *termui, int delta)
|
---|
| 575 | {
|
---|
| 576 | int scrolled = _history_scroll(&termui->history, delta);
|
---|
| 577 |
|
---|
| 578 | if (scrolled != 0 && termui->scroll_cb)
|
---|
| 579 | termui->scroll_cb(termui->scroll_udata, scrolled);
|
---|
| 580 | }
|
---|
| 581 |
|
---|
| 582 | void termui_set_cursor_visibility(termui_t *termui, bool visible)
|
---|
| 583 | {
|
---|
| 584 | if (termui->cursor_visible == visible)
|
---|
| 585 | return;
|
---|
| 586 |
|
---|
| 587 | termui->cursor_visible = visible;
|
---|
| 588 |
|
---|
| 589 | _current_cell(termui).cursor = visible;
|
---|
| 590 | _update_current_cell(termui);
|
---|
| 591 | }
|
---|
| 592 |
|
---|
| 593 | bool termui_get_cursor_visibility(const termui_t *termui)
|
---|
| 594 | {
|
---|
| 595 | return termui->cursor_visible;
|
---|
| 596 | }
|
---|
| 597 |
|
---|
| 598 | static void _termui_put_cells(termui_t *termui, const termui_cell_t *cells, int n)
|
---|
| 599 | {
|
---|
| 600 | while (n > 0) {
|
---|
| 601 | _current_cell(termui) = cells[0];
|
---|
| 602 | cells++;
|
---|
| 603 | n--;
|
---|
| 604 |
|
---|
| 605 | termui->col++;
|
---|
| 606 |
|
---|
| 607 | if (termui->col == termui->cols) {
|
---|
| 608 | _overflow_flag(termui, termui->row) = 1;
|
---|
| 609 | _advance_line(termui);
|
---|
| 610 | termui->col = 0;
|
---|
| 611 | termui->overflow = true;
|
---|
| 612 | } else {
|
---|
| 613 | termui->overflow = false;
|
---|
| 614 | }
|
---|
| 615 | }
|
---|
| 616 |
|
---|
| 617 | if (termui->row >= termui->used_rows)
|
---|
| 618 | termui->used_rows = termui->row + 1;
|
---|
| 619 | }
|
---|
| 620 |
|
---|
| 621 | /** Resize active screen and scrollback depth.
|
---|
| 622 | */
|
---|
| 623 | errno_t termui_resize(termui_t *termui, int cols, int rows, size_t history_lines)
|
---|
| 624 | {
|
---|
| 625 | /* Prevent numerical overflows. */
|
---|
| 626 | if (cols < 2 || rows < 1 || INT_MAX / cols < rows)
|
---|
| 627 | return ERANGE;
|
---|
| 628 |
|
---|
| 629 | int cells = cols * rows;
|
---|
| 630 |
|
---|
| 631 | termui_cell_t *new_screen = calloc(cells, sizeof(new_screen[0]));
|
---|
| 632 | if (!new_screen)
|
---|
| 633 | return ENOMEM;
|
---|
| 634 |
|
---|
| 635 | uint8_t *new_flags = calloc(rows, sizeof(new_flags[0]));
|
---|
| 636 | if (!new_flags) {
|
---|
| 637 | free(new_screen);
|
---|
| 638 | return ENOMEM;
|
---|
| 639 | }
|
---|
| 640 |
|
---|
| 641 | termui_t old_termui = *termui;
|
---|
| 642 |
|
---|
| 643 | termui->rows = rows;
|
---|
| 644 | termui->cols = cols;
|
---|
| 645 | termui->row = 0;
|
---|
| 646 | termui->col = 0;
|
---|
| 647 | termui->used_rows = 0;
|
---|
| 648 | termui->first_row = 0;
|
---|
| 649 | termui->screen = new_screen;
|
---|
| 650 | termui->overflow_flags = new_flags;
|
---|
| 651 | termui->overflow = false;
|
---|
| 652 |
|
---|
| 653 | bool cursor_visible = termui->cursor_visible;
|
---|
| 654 | termui->cursor_visible = false;
|
---|
| 655 |
|
---|
| 656 | termui->history.lines.max_len = history_lines;
|
---|
| 657 |
|
---|
| 658 | if (history_lines > SIZE_MAX / cols)
|
---|
| 659 | termui->history.cells.max_len = SIZE_MAX;
|
---|
| 660 | else
|
---|
| 661 | termui->history.cells.max_len = history_lines * cols;
|
---|
| 662 |
|
---|
| 663 | /* Temporarily remove callbacks. */
|
---|
| 664 | termui->scroll_cb = NULL;
|
---|
| 665 | termui->update_cb = NULL;
|
---|
| 666 | termui->refresh_cb = NULL;
|
---|
| 667 |
|
---|
| 668 | size_t recouped;
|
---|
| 669 | const termui_cell_t *c = _history_reflow(&termui->history, cols, &recouped);
|
---|
| 670 |
|
---|
| 671 | /* Return piece of the incomplete line in scrollback back to active screen. */
|
---|
| 672 | if (recouped > 0)
|
---|
| 673 | _termui_put_cells(termui, c, recouped);
|
---|
| 674 |
|
---|
| 675 | /* Mark cursor position. */
|
---|
| 676 | _current_cell(&old_termui).cursor = 1;
|
---|
| 677 |
|
---|
| 678 | /* Write the contents of old screen into the new one. */
|
---|
| 679 | for (int row = 0; row < old_termui.used_rows; row++) {
|
---|
| 680 | int real_row_offset = _real_row(&old_termui, row) * old_termui.cols;
|
---|
| 681 |
|
---|
| 682 | if (_overflow_flag(&old_termui, row)) {
|
---|
| 683 | _termui_put_cells(termui, &old_termui.screen[real_row_offset], old_termui.cols);
|
---|
| 684 | } else {
|
---|
| 685 | /* Trim trailing blanks. */
|
---|
| 686 | int len = old_termui.cols;
|
---|
| 687 | while (len > 0 && _cell_is_empty(old_termui.screen[real_row_offset + len - 1]))
|
---|
| 688 | len--;
|
---|
| 689 |
|
---|
| 690 | _termui_put_cells(termui, &old_termui.screen[real_row_offset], len);
|
---|
| 691 |
|
---|
| 692 | /* Mark cursor at the end of row, if any. */
|
---|
| 693 | if (len < old_termui.cols)
|
---|
| 694 | _current_cell(termui).cursor = old_termui.screen[real_row_offset + len].cursor;
|
---|
| 695 |
|
---|
| 696 | if (row < old_termui.used_rows - 1)
|
---|
| 697 | termui_put_crlf(termui);
|
---|
| 698 | }
|
---|
| 699 | }
|
---|
| 700 |
|
---|
| 701 | /* Find cursor */
|
---|
| 702 | int new_col = 0;
|
---|
| 703 | int new_row = 0;
|
---|
| 704 | for (int col = 0; col < termui->cols; col++) {
|
---|
| 705 | for (int row = 0; row < termui->rows; row++) {
|
---|
| 706 | if (_screen_cell(termui, col, row).cursor) {
|
---|
| 707 | _screen_cell(termui, col, row).cursor = 0;
|
---|
| 708 | new_col = col;
|
---|
| 709 | new_row = row;
|
---|
| 710 | }
|
---|
| 711 | }
|
---|
| 712 | }
|
---|
| 713 |
|
---|
| 714 | free(old_termui.screen);
|
---|
| 715 | free(old_termui.overflow_flags);
|
---|
| 716 |
|
---|
| 717 | termui->col = new_col;
|
---|
| 718 | termui->row = new_row;
|
---|
| 719 |
|
---|
| 720 | termui->cursor_visible = cursor_visible;
|
---|
| 721 | _cursor_on(termui);
|
---|
| 722 |
|
---|
| 723 | termui->scroll_cb = old_termui.scroll_cb;
|
---|
| 724 | termui->update_cb = old_termui.update_cb;
|
---|
| 725 | termui->refresh_cb = old_termui.refresh_cb;
|
---|
| 726 |
|
---|
| 727 | if (termui->refresh_cb)
|
---|
| 728 | termui->refresh_cb(termui->refresh_udata);
|
---|
| 729 |
|
---|
| 730 | return EOK;
|
---|
| 731 | }
|
---|