| [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 "history.h"
|
|---|
| 30 |
|
|---|
| 31 | #include <assert.h>
|
|---|
| 32 | #include <limits.h>
|
|---|
| 33 | #include <macros.h>
|
|---|
| 34 | #include <mem.h>
|
|---|
| 35 | #include <stdio.h>
|
|---|
| 36 | #include <stdlib.h>
|
|---|
| 37 |
|
|---|
| 38 | #define BLANK_CELLS_LEN 64
|
|---|
| 39 | static const termui_cell_t _blank_cells[BLANK_CELLS_LEN];
|
|---|
| 40 |
|
|---|
| 41 | static bool _lines_empty(struct line_buffer *lines)
|
|---|
| 42 | {
|
|---|
| 43 | return lines->head == lines->tail;
|
|---|
| 44 | }
|
|---|
| 45 |
|
|---|
| 46 | static void _line_idx_inc(const struct line_buffer *lines, size_t *idx)
|
|---|
| 47 | {
|
|---|
| 48 | if (*idx == lines->buf_len - 1)
|
|---|
| 49 | *idx = 0;
|
|---|
| 50 | else
|
|---|
| 51 | (*idx)++;
|
|---|
| 52 | }
|
|---|
| 53 |
|
|---|
| 54 | static void _line_idx_dec(const struct line_buffer *lines, size_t *idx)
|
|---|
| 55 | {
|
|---|
| 56 | if (*idx == 0)
|
|---|
| 57 | *idx = lines->buf_len - 1;
|
|---|
| 58 | else
|
|---|
| 59 | (*idx)--;
|
|---|
| 60 | }
|
|---|
| 61 |
|
|---|
| 62 | ////////////////////////////////////////////////////////////////////////////////
|
|---|
| 63 |
|
|---|
| 64 | static void _cell_buffer_shrink(struct cell_buffer *cells)
|
|---|
| 65 | {
|
|---|
| 66 | assert(cells->max_len > 0);
|
|---|
| 67 | assert(cells->buf_len > cells->max_len);
|
|---|
| 68 |
|
|---|
| 69 | size_t new_len = max(cells->max_len, cells->head_top);
|
|---|
| 70 |
|
|---|
| 71 | termui_cell_t *new_buf = reallocarray(cells->buf,
|
|---|
| 72 | new_len, sizeof(termui_cell_t));
|
|---|
| 73 |
|
|---|
| 74 | if (new_buf) {
|
|---|
| 75 | cells->buf = new_buf;
|
|---|
| 76 | cells->buf_len = new_len;
|
|---|
| 77 | }
|
|---|
| 78 | }
|
|---|
| 79 |
|
|---|
| 80 | static void _line_buffer_shrink(struct line_buffer *lines)
|
|---|
| 81 | {
|
|---|
| 82 | assert(lines->max_len > 0);
|
|---|
| 83 | assert(lines->buf_len > lines->max_len);
|
|---|
| 84 | assert(lines->head <= lines->tail);
|
|---|
| 85 |
|
|---|
| 86 | size_t new_len = max(lines->max_len, lines->tail + 1);
|
|---|
| 87 |
|
|---|
| 88 | struct history_line *new_buf = reallocarray(lines->buf,
|
|---|
| 89 | new_len, sizeof(struct history_line));
|
|---|
| 90 |
|
|---|
| 91 | if (new_buf) {
|
|---|
| 92 | lines->buf = new_buf;
|
|---|
| 93 | lines->buf_len = new_len;
|
|---|
| 94 | }
|
|---|
| 95 | }
|
|---|
| 96 |
|
|---|
| 97 | static void _evict_cells(struct cell_buffer *cells, size_t idx, size_t len)
|
|---|
| 98 | {
|
|---|
| 99 | assert(idx == cells->head_offset);
|
|---|
| 100 | assert(len <= cells->head_top);
|
|---|
| 101 | assert(idx <= cells->head_top - len);
|
|---|
| 102 |
|
|---|
| 103 | cells->head_offset += len;
|
|---|
| 104 |
|
|---|
| 105 | if (cells->head_offset >= cells->head_top) {
|
|---|
| 106 |
|
|---|
| 107 | cells->head_offset = 0;
|
|---|
| 108 | cells->head_top = cells->tail_top;
|
|---|
| 109 | cells->tail_top = 0;
|
|---|
| 110 |
|
|---|
| 111 | if (cells->buf_len > cells->max_len)
|
|---|
| 112 | _cell_buffer_shrink(cells);
|
|---|
| 113 | }
|
|---|
| 114 | }
|
|---|
| 115 |
|
|---|
| 116 | static bool _index_valid(const struct history *history, size_t idx)
|
|---|
| 117 | {
|
|---|
| 118 | const struct line_buffer *lines = &history->lines;
|
|---|
| 119 |
|
|---|
| 120 | if (lines->head <= lines->tail)
|
|---|
| 121 | return idx >= lines->head && idx < lines->tail;
|
|---|
| 122 | else
|
|---|
| 123 | return (idx >= lines->head && idx < lines->buf_len) ||
|
|---|
| 124 | (idx < lines->tail);
|
|---|
| 125 | }
|
|---|
| 126 |
|
|---|
| 127 | #define _history_check(history) do { \
|
|---|
| [9fd74d5] | 128 | assert(history->lines.head < history->lines.buf_len || history->lines.head == 0); \
|
|---|
| 129 | assert(history->lines.tail < history->lines.buf_len || history->lines.tail == 0); \
|
|---|
| [899bdfd] | 130 | assert(history->cells.tail_top <= history->cells.head_offset); \
|
|---|
| 131 | assert(history->cells.head_offset <= history->cells.head_top); \
|
|---|
| 132 | assert(history->cells.head_top <= history->cells.buf_len); \
|
|---|
| 133 | assert(_index_valid(history, history->viewport_top) || history->viewport_top == history->lines.tail); \
|
|---|
| 134 | if (history->append) assert(!_lines_empty(&history->lines)); \
|
|---|
| 135 | } while (false)
|
|---|
| 136 |
|
|---|
| 137 | static void _evict_oldest_line(struct history *history)
|
|---|
| 138 | {
|
|---|
| 139 | struct line_buffer *lines = &history->lines;
|
|---|
| 140 | struct cell_buffer *cells = &history->cells;
|
|---|
| 141 |
|
|---|
| 142 | _history_check(history);
|
|---|
| 143 |
|
|---|
| 144 | bool head = (history->viewport_top == lines->head);
|
|---|
| 145 |
|
|---|
| 146 | struct history_line line = lines->buf[lines->head];
|
|---|
| 147 | _line_idx_inc(lines, &lines->head);
|
|---|
| 148 |
|
|---|
| 149 | if (lines->head == lines->tail) {
|
|---|
| 150 | lines->head = 0;
|
|---|
| 151 | lines->tail = 0;
|
|---|
| 152 | history->viewport_top = 0;
|
|---|
| 153 | history->append = false;
|
|---|
| 154 | history->row_delta = 0;
|
|---|
| 155 | }
|
|---|
| 156 |
|
|---|
| 157 | if (head) {
|
|---|
| 158 | history->viewport_top = lines->head;
|
|---|
| 159 | history->row_delta = 0;
|
|---|
| 160 | }
|
|---|
| 161 |
|
|---|
| 162 | _history_check(history);
|
|---|
| 163 |
|
|---|
| 164 | if (lines->head == 0 && lines->buf_len > lines->max_len)
|
|---|
| 165 | _line_buffer_shrink(lines);
|
|---|
| 166 |
|
|---|
| 167 | _history_check(history);
|
|---|
| 168 |
|
|---|
| 169 | _evict_cells(cells, line.idx, line.len);
|
|---|
| 170 |
|
|---|
| 171 | _history_check(history);
|
|---|
| 172 | }
|
|---|
| 173 |
|
|---|
| 174 | ////////////////////////////////////////////////////////////////////////////////
|
|---|
| 175 |
|
|---|
| 176 | static void _cell_buffer_try_extend(struct cell_buffer *cells, size_t len)
|
|---|
| 177 | {
|
|---|
| 178 | static const size_t MIN_EXTEND_LEN = 128;
|
|---|
| 179 |
|
|---|
| 180 | if (cells->buf_len >= cells->max_len)
|
|---|
| 181 | return;
|
|---|
| 182 |
|
|---|
| 183 | if (cells->tail_top > 0 && len <= cells->buf_len - cells->tail_top) {
|
|---|
| 184 | /*
|
|---|
| 185 | * Don't extend when we will have enough space, since head is gonna get
|
|---|
| 186 | * wiped either way (we don't move already existing lines).
|
|---|
| 187 | * This only matters when allocation has failed previously.
|
|---|
| 188 | */
|
|---|
| 189 | return;
|
|---|
| 190 | }
|
|---|
| 191 |
|
|---|
| 192 | /* Specify a minimum initial allocation size. */
|
|---|
| 193 | len = max(len, MIN_EXTEND_LEN);
|
|---|
| 194 |
|
|---|
| 195 | /* Try to roughly double the buffer size. */
|
|---|
| 196 | len = max(len, cells->buf_len);
|
|---|
| 197 |
|
|---|
| 198 | /* Limit the new size to max_len. */
|
|---|
| 199 | len = min(len, cells->max_len - cells->head_top);
|
|---|
| 200 |
|
|---|
| 201 | size_t new_len =
|
|---|
| 202 | min(cells->head_top + len, SIZE_MAX / sizeof(termui_cell_t));
|
|---|
| 203 |
|
|---|
| 204 | assert(new_len > cells->buf_len);
|
|---|
| 205 | assert(new_len <= cells->max_len);
|
|---|
| 206 |
|
|---|
| 207 | termui_cell_t *new_buf =
|
|---|
| 208 | realloc(cells->buf, new_len * sizeof(termui_cell_t));
|
|---|
| 209 | if (!new_buf) {
|
|---|
| 210 | fprintf(stderr, "termui: Out of memory for scrollback\n");
|
|---|
| 211 | return;
|
|---|
| 212 | }
|
|---|
| 213 |
|
|---|
| 214 | cells->buf = new_buf;
|
|---|
| 215 | cells->buf_len = new_len;
|
|---|
| 216 | }
|
|---|
| 217 |
|
|---|
| 218 | static void _line_buffer_try_extend(struct line_buffer *lines)
|
|---|
| 219 | {
|
|---|
| 220 | static const size_t MIN_EXTEND_LEN = 128;
|
|---|
| 221 |
|
|---|
| 222 | if (lines->buf_len >= lines->max_len)
|
|---|
| 223 | return;
|
|---|
| 224 |
|
|---|
| 225 | if (lines->tail < lines->head)
|
|---|
| 226 | return;
|
|---|
| 227 |
|
|---|
| 228 | /* Specify a minimum initial allocation size. */
|
|---|
| 229 | size_t len = MIN_EXTEND_LEN;
|
|---|
| 230 |
|
|---|
| 231 | /* Try to roughly double the buffer size. */
|
|---|
| 232 | len = max(len, lines->buf_len);
|
|---|
| 233 |
|
|---|
| 234 | /* Limit the new size to max_len. */
|
|---|
| 235 | len = min(len, lines->max_len - lines->buf_len);
|
|---|
| 236 |
|
|---|
| 237 | size_t new_len = min(lines->buf_len + len, SIZE_MAX - sizeof(struct history_line));
|
|---|
| 238 |
|
|---|
| 239 | assert(new_len >= lines->buf_len);
|
|---|
| 240 | assert(new_len <= lines->max_len);
|
|---|
| 241 |
|
|---|
| 242 | if (new_len == lines->buf_len)
|
|---|
| 243 | return;
|
|---|
| 244 |
|
|---|
| 245 | struct history_line *new_buf =
|
|---|
| 246 | realloc(lines->buf, new_len * sizeof(struct history_line));
|
|---|
| 247 | if (!new_buf) {
|
|---|
| 248 | fprintf(stderr, "termui: Out of memory for scrollback\n");
|
|---|
| 249 | return;
|
|---|
| 250 | }
|
|---|
| 251 |
|
|---|
| 252 | lines->buf = new_buf;
|
|---|
| 253 | lines->buf_len = new_len;
|
|---|
| 254 | }
|
|---|
| 255 |
|
|---|
| 256 | static bool _cell_buffer_fits_line(const struct cell_buffer *cells, size_t len)
|
|---|
| 257 | {
|
|---|
| 258 | if (cells->tail_top > 0) {
|
|---|
| 259 | return len <= cells->head_offset - cells->tail_top;
|
|---|
| 260 | } else {
|
|---|
| 261 | return len <= cells->buf_len - cells->head_top || len <= cells->head_offset;
|
|---|
| 262 | }
|
|---|
| 263 | }
|
|---|
| 264 |
|
|---|
| 265 | static struct history_line *_current_line(struct line_buffer *lines)
|
|---|
| 266 | {
|
|---|
| 267 | assert(!_lines_empty(lines));
|
|---|
| 268 | return &lines->buf[(lines->tail ? lines->tail : lines->buf_len) - 1];
|
|---|
| 269 | }
|
|---|
| 270 |
|
|---|
| 271 | static void _alloc_line(struct history *history)
|
|---|
| 272 | {
|
|---|
| 273 | struct line_buffer *lines = &history->lines;
|
|---|
| 274 |
|
|---|
| 275 | size_t idx = 0;
|
|---|
| 276 | if (!_lines_empty(lines))
|
|---|
| 277 | idx = _current_line(lines)->idx + _current_line(lines)->len;
|
|---|
| 278 |
|
|---|
| 279 | if (lines->buf_len == 0) {
|
|---|
| 280 | /* Initial allocation. */
|
|---|
| 281 | _line_buffer_try_extend(lines);
|
|---|
| 282 |
|
|---|
| 283 | if (lines->buf_len == 0) {
|
|---|
| 284 | fprintf(stderr, "termui: Could not allocate initial scrollback buffer\n");
|
|---|
| 285 | return;
|
|---|
| 286 | }
|
|---|
| 287 | }
|
|---|
| 288 |
|
|---|
| 289 | assert(lines->tail < lines->buf_len);
|
|---|
| 290 |
|
|---|
| 291 | bool viewport_inactive = (history->viewport_top == lines->tail);
|
|---|
| 292 |
|
|---|
| 293 | lines->tail++;
|
|---|
| 294 |
|
|---|
| 295 | if (lines->tail >= lines->buf_len)
|
|---|
| 296 | _line_buffer_try_extend(lines);
|
|---|
| 297 |
|
|---|
| 298 | if (lines->tail >= lines->buf_len)
|
|---|
| 299 | lines->tail = 0;
|
|---|
| 300 |
|
|---|
| [9fd74d5] | 301 | if (viewport_inactive) {
|
|---|
| 302 | /* Ensure consistent state so asserts don't trigger inside _evict_oldest_line(). */
|
|---|
| 303 | history->viewport_top = lines->tail;
|
|---|
| 304 | }
|
|---|
| 305 |
|
|---|
| [899bdfd] | 306 | if (lines->tail == lines->head)
|
|---|
| 307 | _evict_oldest_line(history);
|
|---|
| 308 |
|
|---|
| 309 | assert(lines->tail != lines->head);
|
|---|
| 310 |
|
|---|
| 311 | if (viewport_inactive)
|
|---|
| 312 | history->viewport_top = lines->tail;
|
|---|
| 313 |
|
|---|
| 314 | _current_line(lines)->idx = idx;
|
|---|
| 315 | _current_line(lines)->len = 0;
|
|---|
| 316 |
|
|---|
| 317 | history->append = true;
|
|---|
| 318 |
|
|---|
| 319 | _history_check(history);
|
|---|
| 320 | }
|
|---|
| 321 |
|
|---|
| 322 | /** Allocate a line of cells in the cell buffer.
|
|---|
| 323 | * @return Index of first allocated cell in the buffer.
|
|---|
| 324 | */
|
|---|
| 325 | static size_t _alloc_cells(struct cell_buffer *cells, size_t len)
|
|---|
| 326 | {
|
|---|
| 327 | assert(_cell_buffer_fits_line(cells, len));
|
|---|
| 328 |
|
|---|
| 329 | size_t idx;
|
|---|
| 330 |
|
|---|
| 331 | if (cells->tail_top == 0 && cells->buf_len - cells->head_top >= len) {
|
|---|
| 332 | idx = cells->head_top;
|
|---|
| 333 | cells->head_top += len;
|
|---|
| 334 | assert(cells->head_top <= cells->buf_len);
|
|---|
| 335 | } else {
|
|---|
| 336 | idx = cells->tail_top;
|
|---|
| 337 | cells->tail_top += len;
|
|---|
| 338 | assert(cells->tail_top <= cells->head_offset);
|
|---|
| 339 | }
|
|---|
| 340 |
|
|---|
| 341 | return idx;
|
|---|
| 342 | }
|
|---|
| 343 |
|
|---|
| 344 | static termui_cell_t *_history_append(struct history *history, size_t len)
|
|---|
| 345 | {
|
|---|
| 346 | struct line_buffer *lines = &history->lines;
|
|---|
| 347 | struct cell_buffer *cells = &history->cells;
|
|---|
| 348 |
|
|---|
| 349 | /*
|
|---|
| 350 | * Ideally, buffer gets reallocated to its maximum size
|
|---|
| 351 | * before we start recycling it.
|
|---|
| 352 | */
|
|---|
| 353 | if (!_cell_buffer_fits_line(cells, len))
|
|---|
| 354 | _cell_buffer_try_extend(cells, len);
|
|---|
| 355 |
|
|---|
| 356 | if (len > cells->buf_len) {
|
|---|
| 357 | /*
|
|---|
| 358 | * This can only happen if allocation fails early on,
|
|---|
| 359 | * since len is normally limited to row width.
|
|---|
| 360 | */
|
|---|
| 361 | return NULL;
|
|---|
| 362 | }
|
|---|
| 363 |
|
|---|
| 364 | /* Recycle old lines to make space in the buffer. */
|
|---|
| 365 | while (!_cell_buffer_fits_line(cells, len)) {
|
|---|
| 366 | assert(!_lines_empty(lines));
|
|---|
| 367 | _evict_oldest_line(history);
|
|---|
| 368 | }
|
|---|
| 369 |
|
|---|
| 370 | /* Allocate cells for the line. */
|
|---|
| 371 | size_t idx = _alloc_cells(cells, len);
|
|---|
| 372 |
|
|---|
| 373 | /* Allocate the line, if necessary. */
|
|---|
| 374 | if (!history->append || _lines_empty(lines)) {
|
|---|
| 375 | _alloc_line(history);
|
|---|
| 376 |
|
|---|
| 377 | if (_lines_empty(lines)) {
|
|---|
| 378 | /* Initial allocation failed. */
|
|---|
| 379 | return NULL;
|
|---|
| 380 | }
|
|---|
| 381 | }
|
|---|
| 382 |
|
|---|
| 383 | struct history_line *line = _current_line(lines);
|
|---|
| 384 |
|
|---|
| 385 | assert(idx == line->idx + line->len || idx == 0);
|
|---|
| 386 |
|
|---|
| 387 | /* Deal with crossing the buffer's edge. */
|
|---|
| 388 | if (idx != line->idx + line->len) {
|
|---|
| 389 | if (line->len > 0) {
|
|---|
| 390 | /* Breaks off an incomplete line at the end of buffer. */
|
|---|
| 391 | _alloc_line(history);
|
|---|
| 392 | line = _current_line(lines);
|
|---|
| 393 | }
|
|---|
| 394 |
|
|---|
| 395 | line->idx = 0;
|
|---|
| 396 | }
|
|---|
| 397 |
|
|---|
| 398 | line->len += len;
|
|---|
| 399 |
|
|---|
| 400 | return &cells->buf[idx];
|
|---|
| 401 | }
|
|---|
| 402 |
|
|---|
| 403 | /**
|
|---|
| 404 | * @param history
|
|---|
| 405 | * @return True if the top row of the viewport is a scrollback row.
|
|---|
| 406 | */
|
|---|
| 407 | bool _scrollback_active(const struct history *history)
|
|---|
| 408 | {
|
|---|
| 409 | if (history->viewport_top == history->lines.tail)
|
|---|
| 410 | return false;
|
|---|
| 411 |
|
|---|
| 412 | assert(_index_valid(history, history->viewport_top));
|
|---|
| 413 | return true;
|
|---|
| 414 | }
|
|---|
| 415 |
|
|---|
| 416 | static size_t _history_line_rows(const struct history *history, size_t idx)
|
|---|
| 417 | {
|
|---|
| 418 | assert(_index_valid(history, idx));
|
|---|
| 419 |
|
|---|
| 420 | struct history_line line = history->lines.buf[idx];
|
|---|
| 421 |
|
|---|
| 422 | if (line.len == 0)
|
|---|
| 423 | return 1;
|
|---|
| 424 |
|
|---|
| 425 | return (line.len - 1) / history->cols + 1;
|
|---|
| 426 | }
|
|---|
| 427 |
|
|---|
| 428 | static int _history_scroll_down(struct history *history, int requested)
|
|---|
| 429 | {
|
|---|
| 430 | assert(requested > 0);
|
|---|
| 431 |
|
|---|
| 432 | size_t delta = requested;
|
|---|
| 433 |
|
|---|
| 434 | /* Skip first line. */
|
|---|
| 435 |
|
|---|
| 436 | if (history->row_delta > 0) {
|
|---|
| 437 | size_t rows = _history_line_rows(history, history->viewport_top);
|
|---|
| 438 | assert(rows > history->row_delta);
|
|---|
| 439 |
|
|---|
| 440 | if (delta < rows - history->row_delta) {
|
|---|
| 441 | history->row_delta += delta;
|
|---|
| 442 | _history_check(history);
|
|---|
| 443 | return requested;
|
|---|
| 444 | }
|
|---|
| 445 |
|
|---|
| 446 | delta -= rows - history->row_delta;
|
|---|
| 447 | history->row_delta = 0;
|
|---|
| 448 |
|
|---|
| 449 | _line_idx_inc(&history->lines, &history->viewport_top);
|
|---|
| 450 | }
|
|---|
| 451 |
|
|---|
| 452 | /* Skip as many lines as necessary. */
|
|---|
| 453 |
|
|---|
| 454 | while (_scrollback_active(history)) {
|
|---|
| 455 | size_t rows = _history_line_rows(history, history->viewport_top);
|
|---|
| 456 |
|
|---|
| 457 | if (delta < rows) {
|
|---|
| 458 | /* Found the right line. */
|
|---|
| 459 | history->row_delta = delta;
|
|---|
| 460 | _history_check(history);
|
|---|
| 461 | return requested;
|
|---|
| 462 | }
|
|---|
| 463 |
|
|---|
| 464 | delta -= rows;
|
|---|
| 465 |
|
|---|
| 466 | _line_idx_inc(&history->lines, &history->viewport_top);
|
|---|
| 467 | }
|
|---|
| 468 |
|
|---|
| 469 | /* Scrolled past the end of history. */
|
|---|
| 470 | _history_check(history);
|
|---|
| 471 | return requested - delta;
|
|---|
| 472 | }
|
|---|
| 473 |
|
|---|
| 474 | static int _history_scroll_up(struct history *history, int requested)
|
|---|
| 475 | {
|
|---|
| 476 | assert(requested < 0);
|
|---|
| 477 |
|
|---|
| 478 | /* Prevent overflow. */
|
|---|
| 479 | if (history->row_delta > INT_MAX) {
|
|---|
| 480 | history->row_delta += requested;
|
|---|
| 481 | _history_check(history);
|
|---|
| 482 | return requested;
|
|---|
| 483 | }
|
|---|
| 484 |
|
|---|
| 485 | int delta = requested + (int) history->row_delta;
|
|---|
| 486 | history->row_delta = 0;
|
|---|
| 487 |
|
|---|
| 488 | while (delta < 0 && history->viewport_top != history->lines.head) {
|
|---|
| 489 | _line_idx_dec(&history->lines, &history->viewport_top);
|
|---|
| 490 |
|
|---|
| 491 | size_t rows = _history_line_rows(history, history->viewport_top);
|
|---|
| 492 |
|
|---|
| 493 | if (rows > INT_MAX) {
|
|---|
| 494 | history->row_delta = rows + delta;
|
|---|
| 495 | _history_check(history);
|
|---|
| 496 | return requested;
|
|---|
| 497 | }
|
|---|
| 498 |
|
|---|
| 499 | delta += (int) rows;
|
|---|
| 500 | }
|
|---|
| 501 |
|
|---|
| 502 | _history_check(history);
|
|---|
| 503 |
|
|---|
| 504 | if (delta < 0)
|
|---|
| 505 | return requested - delta;
|
|---|
| 506 |
|
|---|
| 507 | assert(delta >= 0);
|
|---|
| 508 | history->row_delta = (size_t) delta;
|
|---|
| 509 | return requested;
|
|---|
| 510 | }
|
|---|
| 511 |
|
|---|
| 512 | static int _history_scroll_to_top(struct history *history)
|
|---|
| 513 | {
|
|---|
| 514 | history->viewport_top = history->lines.head;
|
|---|
| 515 | history->row_delta = 0;
|
|---|
| 516 | _history_check(history);
|
|---|
| 517 | return INT_MIN;
|
|---|
| 518 | }
|
|---|
| 519 |
|
|---|
| 520 | static int _history_scroll_to_bottom(struct history *history)
|
|---|
| 521 | {
|
|---|
| 522 | history->viewport_top = history->lines.tail;
|
|---|
| 523 | _history_check(history);
|
|---|
| 524 | return INT_MAX;
|
|---|
| 525 | }
|
|---|
| 526 |
|
|---|
| 527 | /** Scroll the viewport by the given number of rows.
|
|---|
| 528 | *
|
|---|
| 529 | * @param history
|
|---|
| 530 | * @param delta How many rows to scroll. Negative delta scrolls upward.
|
|---|
| 531 | * @return How many rows have actually been scrolled before top/bottom.
|
|---|
| 532 | */
|
|---|
| 533 | int _history_scroll(struct history *history, int delta)
|
|---|
| 534 | {
|
|---|
| 535 | if (delta == INT_MIN)
|
|---|
| 536 | return _history_scroll_to_top(history);
|
|---|
| 537 | if (delta == INT_MAX)
|
|---|
| 538 | return _history_scroll_to_bottom(history);
|
|---|
| 539 | if (delta > 0)
|
|---|
| 540 | return _history_scroll_down(history, delta);
|
|---|
| 541 | if (delta < 0)
|
|---|
| 542 | return _history_scroll_up(history, delta);
|
|---|
| 543 |
|
|---|
| 544 | return 0;
|
|---|
| 545 | }
|
|---|
| 546 |
|
|---|
| 547 | /** Sets new width for the viewport, recalculating current position so that the
|
|---|
| 548 | * top viewport row remains in place, and returning a piece of the last history
|
|---|
| 549 | * line if the top active screen row is a continuation of it.
|
|---|
| 550 | *
|
|---|
| 551 | * @param history
|
|---|
| 552 | * @param new_cols New column width of the viewport.
|
|---|
| 553 | * @param[out] recouped Number of cells returned to active screen.
|
|---|
| 554 | * @return Pointer to the cell data for returned cells.
|
|---|
| 555 | */
|
|---|
| 556 | const termui_cell_t *_history_reflow(struct history *history, size_t new_cols, size_t *recouped)
|
|---|
| 557 | {
|
|---|
| 558 | history->row_delta = (history->row_delta * history->cols) / new_cols;
|
|---|
| 559 | history->cols = new_cols;
|
|---|
| 560 |
|
|---|
| 561 | if (!history->append) {
|
|---|
| 562 | *recouped = 0;
|
|---|
| 563 | return NULL;
|
|---|
| 564 | }
|
|---|
| 565 |
|
|---|
| 566 | /* Return the part of last line that's not aligned at row boundary. */
|
|---|
| 567 | assert(!_lines_empty(&history->lines));
|
|---|
| 568 |
|
|---|
| 569 | size_t last_idx = history->lines.tail;
|
|---|
| 570 | _line_idx_dec(&history->lines, &last_idx);
|
|---|
| 571 |
|
|---|
| 572 | struct history_line *last = &history->lines.buf[last_idx];
|
|---|
| 573 | *recouped = last->len % new_cols;
|
|---|
| 574 |
|
|---|
| 575 | if (last->idx + last->len == history->cells.head_top) {
|
|---|
| 576 | history->cells.head_top -= *recouped;
|
|---|
| 577 | } else {
|
|---|
| 578 | assert(last->idx + last->len == history->cells.tail_top);
|
|---|
| 579 | history->cells.tail_top -= *recouped;
|
|---|
| 580 | }
|
|---|
| 581 |
|
|---|
| 582 | last->len -= *recouped;
|
|---|
| 583 | if (last->len == 0 && last->idx == 0) {
|
|---|
| 584 | assert(history->cells.tail_top == 0);
|
|---|
| 585 | last->idx = history->cells.head_top;
|
|---|
| 586 | }
|
|---|
| 587 |
|
|---|
| 588 | return &history->cells.buf[last->idx + last->len];
|
|---|
| 589 | }
|
|---|
| 590 |
|
|---|
| 591 | /** Counts the number of scrollback rows present in the viewport.
|
|---|
| 592 | *
|
|---|
| 593 | * @param history
|
|---|
| 594 | * @param max Number of viewport rows.
|
|---|
| 595 | * @return Count.
|
|---|
| 596 | */
|
|---|
| 597 | int _history_viewport_rows(const struct history *history, size_t max)
|
|---|
| 598 | {
|
|---|
| 599 | if (!_scrollback_active(history))
|
|---|
| 600 | return 0;
|
|---|
| 601 |
|
|---|
| 602 | size_t current = history->viewport_top;
|
|---|
| 603 | size_t rows = _history_line_rows(history, current) - history->row_delta;
|
|---|
| 604 | _line_idx_inc(&history->lines, ¤t);
|
|---|
| 605 |
|
|---|
| 606 | while (rows < max && current != history->lines.tail) {
|
|---|
| 607 | rows += _history_line_rows(history, current);
|
|---|
| 608 | _line_idx_inc(&history->lines, ¤t);
|
|---|
| 609 | }
|
|---|
| 610 |
|
|---|
| 611 | return (rows > max) ? max : rows;
|
|---|
| 612 | }
|
|---|
| 613 |
|
|---|
| 614 | static void _update_blank(int col, int row, int len, termui_update_cb_t cb, void *udata)
|
|---|
| 615 | {
|
|---|
| 616 | while (len > BLANK_CELLS_LEN) {
|
|---|
| 617 | cb(udata, col, row, _blank_cells, BLANK_CELLS_LEN);
|
|---|
| 618 | col += BLANK_CELLS_LEN;
|
|---|
| 619 | len -= BLANK_CELLS_LEN;
|
|---|
| 620 | }
|
|---|
| 621 |
|
|---|
| 622 | if (len > 0)
|
|---|
| 623 | cb(udata, col, row, _blank_cells, len);
|
|---|
| 624 | }
|
|---|
| 625 |
|
|---|
| 626 | static void _adjust_row_delta(const struct history *history, size_t *line_idx, size_t *delta)
|
|---|
| 627 | {
|
|---|
| 628 | while (*line_idx != history->lines.tail) {
|
|---|
| 629 | size_t rows = _history_line_rows(history, *line_idx);
|
|---|
| 630 | if (*delta < rows)
|
|---|
| 631 | return;
|
|---|
| 632 |
|
|---|
| 633 | *delta -= rows;
|
|---|
| 634 | _line_idx_inc(&history->lines, line_idx);
|
|---|
| 635 | }
|
|---|
| 636 | }
|
|---|
| 637 |
|
|---|
| 638 | /** Run update callback for a range of visible scrollback srows.
|
|---|
| 639 | *
|
|---|
| 640 | * @param history
|
|---|
| 641 | * @param row First viewport row we want to update.
|
|---|
| 642 | * @param count Number of viewport rows we want to update.
|
|---|
| 643 | * @param cb Callback to call for every row.
|
|---|
| 644 | * @param udata Callback userdata.
|
|---|
| 645 | * @return Actual number of rows updated (may be less than count if
|
|---|
| 646 | * the rest of rows are from the active screen).
|
|---|
| 647 | */
|
|---|
| 648 | int _history_iter_rows(const struct history *history, int row, int count, termui_update_cb_t cb, void *udata)
|
|---|
| 649 | {
|
|---|
| 650 | assert(history->row_delta <= SIZE_MAX - row);
|
|---|
| 651 |
|
|---|
| 652 | size_t current_line = history->viewport_top;
|
|---|
| 653 | size_t delta = history->row_delta + (size_t) row;
|
|---|
| 654 | /* Get to the first row to be returned. */
|
|---|
| 655 | _adjust_row_delta(history, ¤t_line, &delta);
|
|---|
| 656 |
|
|---|
| 657 | int initial_count = count;
|
|---|
| 658 |
|
|---|
| 659 | while (count > 0 && current_line != history->lines.tail) {
|
|---|
| 660 | /* Process each line. */
|
|---|
| 661 | assert(_index_valid(history, current_line));
|
|---|
| 662 |
|
|---|
| 663 | struct history_line line = history->lines.buf[current_line];
|
|---|
| 664 | assert(line.len <= history->cells.buf_len);
|
|---|
| 665 | assert(line.idx <= history->cells.buf_len - line.len);
|
|---|
| 666 |
|
|---|
| 667 | if (line.len == 0) {
|
|---|
| 668 | /* Special case for empty line. */
|
|---|
| 669 | _update_blank(0, row, history->cols, cb, udata);
|
|---|
| 670 | row++;
|
|---|
| 671 | count--;
|
|---|
| 672 | _line_idx_inc(&history->lines, ¤t_line);
|
|---|
| 673 | continue;
|
|---|
| 674 | }
|
|---|
| 675 |
|
|---|
| 676 | const termui_cell_t *cells = &history->cells.buf[line.idx];
|
|---|
| 677 | size_t line_offset = delta * history->cols;
|
|---|
| 678 | assert(line_offset < line.len);
|
|---|
| 679 | delta = 0;
|
|---|
| 680 |
|
|---|
| 681 | /* Callback for each full row. */
|
|---|
| 682 | while (count > 0 && line_offset + history->cols <= line.len) {
|
|---|
| 683 | assert(line.idx + line_offset <= history->cells.buf_len - history->cols);
|
|---|
| 684 | cb(udata, 0, row, &cells[line_offset], history->cols);
|
|---|
| 685 |
|
|---|
| 686 | line_offset += history->cols;
|
|---|
| 687 | row++;
|
|---|
| 688 | count--;
|
|---|
| 689 | }
|
|---|
| 690 |
|
|---|
| 691 | if (count > 0 && line_offset < line.len) {
|
|---|
| 692 | /* Callback for the last (incomplete) row. */
|
|---|
| 693 |
|
|---|
| 694 | cb(udata, 0, row, &cells[line_offset], line.len - line_offset);
|
|---|
| 695 |
|
|---|
| 696 | size_t col = line.len - line_offset;
|
|---|
| 697 | assert(col < history->cols);
|
|---|
| 698 |
|
|---|
| 699 | /* Callbacks for the blank section in the last row. */
|
|---|
| 700 | _update_blank(col, row, history->cols - col, cb, udata);
|
|---|
| 701 |
|
|---|
| 702 | row++;
|
|---|
| 703 | count--;
|
|---|
| 704 | }
|
|---|
| 705 |
|
|---|
| 706 | _line_idx_inc(&history->lines, ¤t_line);
|
|---|
| 707 | }
|
|---|
| 708 |
|
|---|
| 709 | return initial_count - count;
|
|---|
| 710 | }
|
|---|
| 711 |
|
|---|
| 712 | /** Append a row from active screen to scrollback history.
|
|---|
| 713 | *
|
|---|
| 714 | * @param history
|
|---|
| 715 | * @param b Pointer to the row in active screen buffer.
|
|---|
| 716 | * @param last False if the row was overflowed, meaning the next row will be
|
|---|
| 717 | * appended to the same history line as this row.
|
|---|
| 718 | */
|
|---|
| 719 | void _history_append_row(struct history *history, const termui_cell_t *b, bool last)
|
|---|
| 720 | {
|
|---|
| 721 | size_t len = history->cols;
|
|---|
| 722 |
|
|---|
| 723 | /* Reduce multiple trailing empty cells to just one. */
|
|---|
| 724 | if (last) {
|
|---|
| 725 | while (len > 1 && _cell_is_empty(b[len - 1]) && _cell_is_empty(b[len - 2]))
|
|---|
| 726 | len--;
|
|---|
| 727 | }
|
|---|
| 728 |
|
|---|
| 729 | memcpy(_history_append(history, len), b, sizeof(termui_cell_t) * len);
|
|---|
| 730 |
|
|---|
| 731 | if (last)
|
|---|
| 732 | history->append = false;
|
|---|
| 733 | }
|
|---|