source: mainline/uspace/lib/termui/src/history.c@ 9fd74d5

Last change on this file since 9fd74d5 was 9fd74d5, checked in by Jiří Zárevúcky <zarevucky.jiri@…>, 3 days ago

libtermui: Ensure fully consistent history state when evicting old data

Fixes bug #890

  • Property mode set to 100644
File size: 18.9 KB
RevLine 
[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
39static const termui_cell_t _blank_cells[BLANK_CELLS_LEN];
40
41static bool _lines_empty(struct line_buffer *lines)
42{
43 return lines->head == lines->tail;
44}
45
46static 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
54static 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
64static 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
80static 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
97static 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
116static 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
137static 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
176static 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
218static 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
256static 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
265static 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
271static 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 */
325static 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
344static 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 */
407bool _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
416static 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
428static 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
474static 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
512static 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
520static 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 */
533int _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 */
556const 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 */
597int _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, &current);
605
606 while (rows < max && current != history->lines.tail) {
607 rows += _history_line_rows(history, current);
608 _line_idx_inc(&history->lines, &current);
609 }
610
611 return (rows > max) ? max : rows;
612}
613
614static 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
626static 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 */
648int _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, &current_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, &current_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, &current_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 */
719void _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}
Note: See TracBrowser for help on using the repository browser.