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

Last change on this file since ca22536 was 88739997, checked in by Jiri Svoboda <jiri@…>, 11 months ago

Remove forgotten debug messages

  • Property mode set to 100644
File size: 18.6 KB
Line 
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 { \
128 assert(history->lines.head < history->lines.buf_len); \
129 assert(history->lines.tail < history->lines.buf_len); \
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
301 if (lines->tail == lines->head)
302 _evict_oldest_line(history);
303
304 assert(lines->tail != lines->head);
305
306 if (viewport_inactive)
307 history->viewport_top = lines->tail;
308
309 _current_line(lines)->idx = idx;
310 _current_line(lines)->len = 0;
311
312 history->append = true;
313
314 _history_check(history);
315}
316
317/** Allocate a line of cells in the cell buffer.
318 * @return Index of first allocated cell in the buffer.
319 */
320static size_t _alloc_cells(struct cell_buffer *cells, size_t len)
321{
322 assert(_cell_buffer_fits_line(cells, len));
323
324 size_t idx;
325
326 if (cells->tail_top == 0 && cells->buf_len - cells->head_top >= len) {
327 idx = cells->head_top;
328 cells->head_top += len;
329 assert(cells->head_top <= cells->buf_len);
330 } else {
331 idx = cells->tail_top;
332 cells->tail_top += len;
333 assert(cells->tail_top <= cells->head_offset);
334 }
335
336 return idx;
337}
338
339static termui_cell_t *_history_append(struct history *history, size_t len)
340{
341 struct line_buffer *lines = &history->lines;
342 struct cell_buffer *cells = &history->cells;
343
344 /*
345 * Ideally, buffer gets reallocated to its maximum size
346 * before we start recycling it.
347 */
348 if (!_cell_buffer_fits_line(cells, len))
349 _cell_buffer_try_extend(cells, len);
350
351 if (len > cells->buf_len) {
352 /*
353 * This can only happen if allocation fails early on,
354 * since len is normally limited to row width.
355 */
356 return NULL;
357 }
358
359 /* Recycle old lines to make space in the buffer. */
360 while (!_cell_buffer_fits_line(cells, len)) {
361 assert(!_lines_empty(lines));
362 _evict_oldest_line(history);
363 }
364
365 /* Allocate cells for the line. */
366 size_t idx = _alloc_cells(cells, len);
367
368 /* Allocate the line, if necessary. */
369 if (!history->append || _lines_empty(lines)) {
370 _alloc_line(history);
371
372 if (_lines_empty(lines)) {
373 /* Initial allocation failed. */
374 return NULL;
375 }
376 }
377
378 struct history_line *line = _current_line(lines);
379
380 assert(idx == line->idx + line->len || idx == 0);
381
382 /* Deal with crossing the buffer's edge. */
383 if (idx != line->idx + line->len) {
384 if (line->len > 0) {
385 /* Breaks off an incomplete line at the end of buffer. */
386 _alloc_line(history);
387 line = _current_line(lines);
388 }
389
390 line->idx = 0;
391 }
392
393 line->len += len;
394
395 return &cells->buf[idx];
396}
397
398/**
399 * @param history
400 * @return True if the top row of the viewport is a scrollback row.
401 */
402bool _scrollback_active(const struct history *history)
403{
404 if (history->viewport_top == history->lines.tail)
405 return false;
406
407 assert(_index_valid(history, history->viewport_top));
408 return true;
409}
410
411static size_t _history_line_rows(const struct history *history, size_t idx)
412{
413 assert(_index_valid(history, idx));
414
415 struct history_line line = history->lines.buf[idx];
416
417 if (line.len == 0)
418 return 1;
419
420 return (line.len - 1) / history->cols + 1;
421}
422
423static int _history_scroll_down(struct history *history, int requested)
424{
425 assert(requested > 0);
426
427 size_t delta = requested;
428
429 /* Skip first line. */
430
431 if (history->row_delta > 0) {
432 size_t rows = _history_line_rows(history, history->viewport_top);
433 assert(rows > history->row_delta);
434
435 if (delta < rows - history->row_delta) {
436 history->row_delta += delta;
437 _history_check(history);
438 return requested;
439 }
440
441 delta -= rows - history->row_delta;
442 history->row_delta = 0;
443
444 _line_idx_inc(&history->lines, &history->viewport_top);
445 }
446
447 /* Skip as many lines as necessary. */
448
449 while (_scrollback_active(history)) {
450 size_t rows = _history_line_rows(history, history->viewport_top);
451
452 if (delta < rows) {
453 /* Found the right line. */
454 history->row_delta = delta;
455 _history_check(history);
456 return requested;
457 }
458
459 delta -= rows;
460
461 _line_idx_inc(&history->lines, &history->viewport_top);
462 }
463
464 /* Scrolled past the end of history. */
465 _history_check(history);
466 return requested - delta;
467}
468
469static int _history_scroll_up(struct history *history, int requested)
470{
471 assert(requested < 0);
472
473 /* Prevent overflow. */
474 if (history->row_delta > INT_MAX) {
475 history->row_delta += requested;
476 _history_check(history);
477 return requested;
478 }
479
480 int delta = requested + (int) history->row_delta;
481 history->row_delta = 0;
482
483 while (delta < 0 && history->viewport_top != history->lines.head) {
484 _line_idx_dec(&history->lines, &history->viewport_top);
485
486 size_t rows = _history_line_rows(history, history->viewport_top);
487
488 if (rows > INT_MAX) {
489 history->row_delta = rows + delta;
490 _history_check(history);
491 return requested;
492 }
493
494 delta += (int) rows;
495 }
496
497 _history_check(history);
498
499 if (delta < 0)
500 return requested - delta;
501
502 assert(delta >= 0);
503 history->row_delta = (size_t) delta;
504 return requested;
505}
506
507static int _history_scroll_to_top(struct history *history)
508{
509 history->viewport_top = history->lines.head;
510 history->row_delta = 0;
511 _history_check(history);
512 return INT_MIN;
513}
514
515static int _history_scroll_to_bottom(struct history *history)
516{
517 history->viewport_top = history->lines.tail;
518 _history_check(history);
519 return INT_MAX;
520}
521
522/** Scroll the viewport by the given number of rows.
523 *
524 * @param history
525 * @param delta How many rows to scroll. Negative delta scrolls upward.
526 * @return How many rows have actually been scrolled before top/bottom.
527 */
528int _history_scroll(struct history *history, int delta)
529{
530 if (delta == INT_MIN)
531 return _history_scroll_to_top(history);
532 if (delta == INT_MAX)
533 return _history_scroll_to_bottom(history);
534 if (delta > 0)
535 return _history_scroll_down(history, delta);
536 if (delta < 0)
537 return _history_scroll_up(history, delta);
538
539 return 0;
540}
541
542/** Sets new width for the viewport, recalculating current position so that the
543 * top viewport row remains in place, and returning a piece of the last history
544 * line if the top active screen row is a continuation of it.
545 *
546 * @param history
547 * @param new_cols New column width of the viewport.
548 * @param[out] recouped Number of cells returned to active screen.
549 * @return Pointer to the cell data for returned cells.
550 */
551const termui_cell_t *_history_reflow(struct history *history, size_t new_cols, size_t *recouped)
552{
553 history->row_delta = (history->row_delta * history->cols) / new_cols;
554 history->cols = new_cols;
555
556 if (!history->append) {
557 *recouped = 0;
558 return NULL;
559 }
560
561 /* Return the part of last line that's not aligned at row boundary. */
562 assert(!_lines_empty(&history->lines));
563
564 size_t last_idx = history->lines.tail;
565 _line_idx_dec(&history->lines, &last_idx);
566
567 struct history_line *last = &history->lines.buf[last_idx];
568 *recouped = last->len % new_cols;
569
570 if (last->idx + last->len == history->cells.head_top) {
571 history->cells.head_top -= *recouped;
572 } else {
573 assert(last->idx + last->len == history->cells.tail_top);
574 history->cells.tail_top -= *recouped;
575 }
576
577 last->len -= *recouped;
578 if (last->len == 0 && last->idx == 0) {
579 assert(history->cells.tail_top == 0);
580 last->idx = history->cells.head_top;
581 }
582
583 return &history->cells.buf[last->idx + last->len];
584}
585
586/** Counts the number of scrollback rows present in the viewport.
587 *
588 * @param history
589 * @param max Number of viewport rows.
590 * @return Count.
591 */
592int _history_viewport_rows(const struct history *history, size_t max)
593{
594 if (!_scrollback_active(history))
595 return 0;
596
597 size_t current = history->viewport_top;
598 size_t rows = _history_line_rows(history, current) - history->row_delta;
599 _line_idx_inc(&history->lines, &current);
600
601 while (rows < max && current != history->lines.tail) {
602 rows += _history_line_rows(history, current);
603 _line_idx_inc(&history->lines, &current);
604 }
605
606 return (rows > max) ? max : rows;
607}
608
609static void _update_blank(int col, int row, int len, termui_update_cb_t cb, void *udata)
610{
611 while (len > BLANK_CELLS_LEN) {
612 cb(udata, col, row, _blank_cells, BLANK_CELLS_LEN);
613 col += BLANK_CELLS_LEN;
614 len -= BLANK_CELLS_LEN;
615 }
616
617 if (len > 0)
618 cb(udata, col, row, _blank_cells, len);
619}
620
621static void _adjust_row_delta(const struct history *history, size_t *line_idx, size_t *delta)
622{
623 while (*line_idx != history->lines.tail) {
624 size_t rows = _history_line_rows(history, *line_idx);
625 if (*delta < rows)
626 return;
627
628 *delta -= rows;
629 _line_idx_inc(&history->lines, line_idx);
630 }
631}
632
633/** Run update callback for a range of visible scrollback srows.
634 *
635 * @param history
636 * @param row First viewport row we want to update.
637 * @param count Number of viewport rows we want to update.
638 * @param cb Callback to call for every row.
639 * @param udata Callback userdata.
640 * @return Actual number of rows updated (may be less than count if
641 * the rest of rows are from the active screen).
642 */
643int _history_iter_rows(const struct history *history, int row, int count, termui_update_cb_t cb, void *udata)
644{
645 assert(history->row_delta <= SIZE_MAX - row);
646
647 size_t current_line = history->viewport_top;
648 size_t delta = history->row_delta + (size_t) row;
649 /* Get to the first row to be returned. */
650 _adjust_row_delta(history, &current_line, &delta);
651
652 int initial_count = count;
653
654 while (count > 0 && current_line != history->lines.tail) {
655 /* Process each line. */
656 assert(_index_valid(history, current_line));
657
658 struct history_line line = history->lines.buf[current_line];
659 assert(line.len <= history->cells.buf_len);
660 assert(line.idx <= history->cells.buf_len - line.len);
661
662 if (line.len == 0) {
663 /* Special case for empty line. */
664 _update_blank(0, row, history->cols, cb, udata);
665 row++;
666 count--;
667 _line_idx_inc(&history->lines, &current_line);
668 continue;
669 }
670
671 const termui_cell_t *cells = &history->cells.buf[line.idx];
672 size_t line_offset = delta * history->cols;
673 assert(line_offset < line.len);
674 delta = 0;
675
676 /* Callback for each full row. */
677 while (count > 0 && line_offset + history->cols <= line.len) {
678 assert(line.idx + line_offset <= history->cells.buf_len - history->cols);
679 cb(udata, 0, row, &cells[line_offset], history->cols);
680
681 line_offset += history->cols;
682 row++;
683 count--;
684 }
685
686 if (count > 0 && line_offset < line.len) {
687 /* Callback for the last (incomplete) row. */
688
689 cb(udata, 0, row, &cells[line_offset], line.len - line_offset);
690
691 size_t col = line.len - line_offset;
692 assert(col < history->cols);
693
694 /* Callbacks for the blank section in the last row. */
695 _update_blank(col, row, history->cols - col, cb, udata);
696
697 row++;
698 count--;
699 }
700
701 _line_idx_inc(&history->lines, &current_line);
702 }
703
704 return initial_count - count;
705}
706
707/** Append a row from active screen to scrollback history.
708 *
709 * @param history
710 * @param b Pointer to the row in active screen buffer.
711 * @param last False if the row was overflowed, meaning the next row will be
712 * appended to the same history line as this row.
713 */
714void _history_append_row(struct history *history, const termui_cell_t *b, bool last)
715{
716 size_t len = history->cols;
717
718 /* Reduce multiple trailing empty cells to just one. */
719 if (last) {
720 while (len > 1 && _cell_is_empty(b[len - 1]) && _cell_is_empty(b[len - 2]))
721 len--;
722 }
723
724 memcpy(_history_append(history, len), b, sizeof(termui_cell_t) * len);
725
726 if (last)
727 history->append = false;
728}
Note: See TracBrowser for help on using the repository browser.