source: mainline/uspace/lib/termui/src/termui.c

Last change on this file was 2cf8f994, checked in by Jiri Svoboda <jiri@…>, 10 months ago

Improve terminal behavior in console mode.

  • Property mode set to 100644
File size: 17.7 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 <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
38struct 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
75static 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 */
95void termui_set_style(termui_t *termui, termui_cell_t style)
96{
97 termui->style = style;
98}
99
100static 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 */
139termui_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
147static 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
157static void _update_current_cell(termui_t *termui)
158{
159 _update_active_cells(termui, termui->col, termui->row, 1);
160}
161
162static 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
170static 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
178static 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
192 if (termui->rows > 1)
193 termui->row++;
194
195 if (termui->row >= termui->used_rows)
196 termui->used_rows = termui->row + 1;
197
198 assert(termui->row < termui->rows);
199}
200
201void 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
209void 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. */
228void 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
247void 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
261void 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 */
288void 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
340termui_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
349void 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 */
372int termui_get_cols(const termui_t *termui)
373{
374 return termui->cols;
375}
376
377/** Get terminal height.
378 */
379int termui_get_rows(const termui_t *termui)
380{
381 return termui->rows;
382}
383
384/** Get cursor position
385 */
386void 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 */
394void 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 */
419void 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 */
449void 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
461 _overflow_flag(termui, row) = false;
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
475void 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
481void 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
487void 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 */
496void 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
518bool termui_scrollback_is_active(const termui_t *termui)
519{
520 return _scrollback_active(&termui->history);
521}
522
523termui_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
560void 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 */
574void 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
582void 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
593bool termui_get_cursor_visibility(const termui_t *termui)
594{
595 return termui->cursor_visible;
596}
597
598static 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 */
623errno_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}
Note: See TracBrowser for help on using the repository browser.