foot/terminal.c
Daniel Eklöf 4e25019ba6
wip: grid is now represented as a grid, not a linear array
The grid is now represented with an array of row *pointers*. Each row
contains an array of cells (the row's columns).

The main point of having row pointers is we can now move rows around
almost for free.

This is useful when scrolling with scroll margins for example, where
we previously had to copy the lines in the margins. Now it's just a
matter of swapping two pointers.
2019-07-08 13:57:31 +02:00

637 lines
17 KiB
C

#include "terminal.h"
#include <string.h>
#include <unistd.h>
#include <assert.h>
#define LOG_MODULE "terminal"
#define LOG_ENABLE_DBG 0
#include "log.h"
#include "grid.h"
#define min(x, y) ((x) < (y) ? (x) : (y))
#define max(x, y) ((x) > (y) ? (x) : (y))
#if 0
static bool
damage_merge_range(struct terminal *term, const struct damage *dmg)
{
if (tll_length(term->grid->damage) == 0)
return false;
struct damage *old = &tll_back(term->grid->damage);
if (old->type != dmg->type)
return false;
const int start = dmg->range.start;
const int end = start + dmg->range.length;
const int prev_start = old->range.start;
const int prev_end = prev_start + old->range.length;
if ((start >= prev_start && start <= prev_end) ||
(end >= prev_start && end <= prev_end) ||
(start <= prev_start && end >= prev_end))
{
/* The two damage ranges intersect */
int new_start = min(start, prev_start);
int new_end = max(end, prev_end);
old->range.start = new_start;
old->range.length = new_end - new_start;
return true;
}
return false;
}
static void
term_damage_update_or_erase(struct terminal *term, enum damage_type damage_type,
int start, int length)
{
#if 1
if (tll_length(term->grid->damage) > 1024) {
term_damage_all(term);
return;
}
#endif
struct damage dmg = {
.type = damage_type,
.range = {.start = term->grid->offset + start, .length = length},
};
if (damage_merge_range(term, &dmg))
return;
tll_push_back(term->grid->damage, dmg);
}
#endif
void
term_damage_update(struct terminal *term, int start, int length)
{
#if 0
assert(start + length <= term->rows * term->cols);
term_damage_update_or_erase(term, DAMAGE_UPDATE, start, length);
#endif
}
void
term_damage_erase(struct terminal *term, int start, int length)
{
#if 0
assert(start + length <= term->rows * term->cols);
term_damage_update_or_erase(term, DAMAGE_ERASE, start, length);
#endif
}
void
term_damage_all(struct terminal *term)
{
#if 0
tll_free(term->grid->damage);
tll_free(term->grid->scroll_damage);
term_damage_update(term, 0, term->rows * term->cols);
#else
for (int i = 0; i < term->rows; i++)
grid_row(term->grid, i)->dirty = true;
#endif
}
#if 0
static void
damage_adjust_after_scroll(struct terminal *term, enum damage_type damage_type,
struct scroll_region region, int lines)
{
tll_foreach(term->grid->damage, it) {
#if 0
if (it->item.range.start < term->grid->offset) {
int end = it->item.range.start + it->item.range.length;
if (end >= term->grid->offset) {
it->item.range.start = term->grid->offset;
it->item.range.length = end - it->item.range.start;
} else {
tll_remove(term->grid->damage, it);
}
}
#endif
int start = it->item.range.start;
int end = start + it->item.range.length;
if (start -
}
}
#endif
void
term_damage_scroll(struct terminal *term, enum damage_type damage_type,
struct scroll_region region, int lines)
{
#if 0
//damage_adjust_after_scroll(term, damage_type, region, lines);
if (damage_type == DAMAGE_SCROLL) {
tll_foreach(term->grid->damage, it) {
int start = it->item.range.start;
int length = it->item.range.length;
if (start < term->grid->offset) {
int end = start + length;
if (end >= term->grid->offset) {
it->item.range.start = term->grid->offset;
it->item.range.length = end - it->item.range.start;
} else
tll_remove(term->grid->damage, it);
} else
break;
}
}
#endif
if (tll_length(term->grid->scroll_damage) > 0) {
struct damage *dmg = &tll_back(term->grid->scroll_damage);
if (dmg->type == damage_type &&
dmg->scroll.region.start == region.start &&
dmg->scroll.region.end == region.end)
{
dmg->scroll.lines += lines;
return;
}
}
struct damage dmg = {
.type = damage_type,
.scroll = {.region = region, .lines = lines},
};
tll_push_back(term->grid->scroll_damage, dmg);
}
#if 0
void
term_erase(struct terminal *term, int start, int end)
{
LOG_DBG("erase: %d-%d", start, end);
assert(end >= start);
assert(end <= term->rows * term->cols);
if (term->vt.attrs.have_background) {
int erase_start = start;
int left = end - erase_start;
while (left > 0) {
int len = left;
struct cell *cells = grid_get_range(term->grid, erase_start, &len);
memset(cells, 0, len * sizeof(cells[0]));
for (int i = 0; i < len; i++) {
cells[i].attrs.background = term->vt.attrs.background;
cells[i].attrs.have_background = true;
}
erase_start += len;
left -= len;
}
term_damage_update(term, start, end - start);
} else {
grid_memclear(term->grid, start, end - start);
term_damage_erase(term, start, end - start);
}
}
#else
static inline void
erase_cell_range(struct terminal *term, struct row *row, int start, int end)
{
assert(start < term->cols);
assert(end < term->cols);
if (unlikely(term->vt.attrs.have_background)) {
for (int col = start; col <= end; col++) {
row->cells[col].c[0] = '\0';
row->cells[col].attrs.have_background = true;
row->cells[col].attrs.background = term->vt.attrs.background;
}
} else {
memset(&row->cells[start], 0, (end - start + 1) * sizeof(row->cells[0]));
}
row->dirty = true;
}
static inline void
erase_line(struct terminal *term, struct row *row)
{
erase_cell_range(term, row, 0, term->cols - 1);
}
void
term_erase(struct terminal *term, const struct coord *start, const struct coord *end)
{
assert(start->row <= end->row);
assert(start->col <= end->col || start->row < end->row);
if (start->row == end->row) {
struct row *row = grid_row(term->grid, start->row);
erase_cell_range(term, row, start->col, end->col);
return;
}
assert(end->row > start->row);
erase_cell_range(
term, grid_row(term->grid, start->row), start->col, term->cols - 1);
for (int r = start->row + 1; r < end->row; r++)
erase_line(term, grid_row(term->grid, r));
erase_cell_range(term, grid_row(term->grid, end->row), 0, end->col);
}
#endif
void
term_cursor_to(struct terminal *term, int row, int col)
{
assert(row < term->rows);
assert(col < term->cols);
term->print_needs_wrap = false;
term->cursor.col = col;
term->cursor.row = row;
term->grid->cur_row = grid_row(term->grid, row);
}
void
term_cursor_left(struct terminal *term, int count)
{
int move_amount = min(term->cursor.col, count);
term->cursor.col -= move_amount;
assert(term->cursor.col >= 0);
term->print_needs_wrap = false;
}
void
term_cursor_right(struct terminal *term, int count)
{
int move_amount = min(term->cols - term->cursor.col - 1, count);
term->cursor.col += move_amount;
assert(term->cursor.col < term->cols);
term->print_needs_wrap = false;
}
void
term_cursor_up(struct terminal *term, int count)
{
int move_amount = min(term->cursor.row, count);
term_cursor_to(term, term->cursor.row - move_amount, term->cursor.col);
}
void
term_cursor_down(struct terminal *term, int count)
{
int move_amount = min(term->rows - term->cursor.row - 1, count);
term_cursor_to(term, term->cursor.row + move_amount, term->cursor.col);
}
void
term_scroll_partial(struct terminal *term, struct scroll_region region, int rows)
{
LOG_DBG("scroll: %d rows", rows);
assert(rows < term->rows && "unimplemented");
for (int i = region.start - 1; i >= 0; i--)
grid_swap_row(term->grid, i, i + rows);
if (region.start > 0) {
#if 0
tll_foreach(term->grid->damage, it) {
int start = it->item.range.start - term->grid->offset;
int end __attribute__((unused)) = start + it->item.range.length;
if (start < region.start * term->cols) {
assert(end <= region.start * term->cols);
it->item.range.start += rows * term->cols;
}
}
#endif
}
for (int i = term->rows - 1; i >= region.end; i--)
grid_swap_row(term->grid, i, i + rows);
if (region.end < term->rows) {
#if 0
grid_memmove(
term->grid,
(region.end + rows) * term->cols,
region.end * term->cols,
(term->rows - region.end) * term->cols);
#endif
#if 0
tll_foreach(term->grid->damage, it) {
int start = it->item.range.start - term->grid->offset;
int end = start + it->item.range.length;
if (end > region.end * term->cols) {
assert(start >= region.end * term->cols);
it->item.range.start += rows * term->cols;
}
}
#endif
}
/* Offset grid origin */
term->grid->offset += rows;
term->grid->offset %= term->grid->num_rows;
#if 0
term_erase(
term,
&(struct coord){0, max(region.end - rows, 0)},
&(struct coord){term->cols - 1, region.end - 1});
#else
for (int r = max(region.end - rows, 0); r < region.end; r++)
erase_line(term, grid_row(term->grid, r));
#endif
term_damage_scroll(term, DAMAGE_SCROLL, region, rows);
term->grid->cur_row = grid_row(term->grid, term->cursor.row);
#if 0
int len = term->cols;
term->grid->cur_line = grid_get_range(
term->grid, term->cursor.linear - term->cursor.col, &len);
assert(len == term->cols);
#endif
}
void
term_scroll(struct terminal *term, int rows)
{
term_scroll_partial(term, term->scroll_region, rows);
}
void
term_scroll_reverse_partial(struct terminal *term,
struct scroll_region region, int rows)
{
assert(rows < term->rows && "unimplemented");
if (region.end < term->rows) {
//assert(false);
#if 0
grid_memmove(
term->grid,
(region.end - rows) * term->cols,
region.end * term->cols,
(term->rows - region.end) * term->cols);
tll_foreach(term->grid->damage, it) {
int start = it->item.range.start - term->grid->offset;
int end = start + it->item.range.length;
if (end > region.end * term->cols) {
assert(start >= region.end * term->cols);
it->item.range.start -= rows * term->cols;
}
}
#endif
}
if (region.start > 0) {
//assert(false);
#if 0
grid_memmove(
term->grid, -rows * term->cols, 0, region.start * term->cols);
tll_foreach(term->grid->damage, it) {
int start = it->item.range.start - term->grid->offset;
int end __attribute__((unused)) = start + it->item.range.length;
if (start < region.start * term->cols) {
if (end > region.start * term->cols) {
LOG_ERR("region.start = %d, rows = %d, damage.start = %d, damage.end = %d (%s)",
region.start, rows, start, end, it->item.type == DAMAGE_UPDATE ? "UPDATE" : "ERASE");
abort();
}
assert(end <= region.start * term->cols);
it->item.range.start -= rows * term->cols;
}
}
#endif
}
term->grid->offset += term->grid->num_rows - rows;
term->grid->offset %= term->grid->num_rows;
for (int i = region.end + rows; i < term->rows + rows; i++)
grid_swap_row(term->grid, i, i - rows);
for (int i = 0 + rows; i < region.start + rows; i++)
grid_swap_row(term->grid, i, i - rows);
term_erase(
term,
&(struct coord){0, region.start},
&(struct coord){term->cols - 1, min(region.start + rows, region.end) - 1});
term_damage_scroll(term, DAMAGE_SCROLL_REVERSE, region, rows);
term->grid->cur_row = grid_row(term->grid, term->cursor.row);
#if 0
int len = term->cols;
term->grid->cur_line = grid_get_range(
term->grid, term->cursor.linear - term->cursor.col, &len);
assert(len == term->cols);
#endif
}
void
term_scroll_reverse(struct terminal *term, int rows)
{
term_scroll_reverse_partial(term, term->scroll_region, rows);
}
#include <linux/input-event-codes.h>
static int
linux_mouse_button_to_x(int button)
{
switch (button) {
case BTN_LEFT: return 1;
case BTN_RIGHT: return 3;
case BTN_MIDDLE: return 2;
case BTN_SIDE: return 8;
case BTN_EXTRA: return 9;
case BTN_FORWARD: return 4;
case BTN_BACK: return 5;
case BTN_TASK: return -1; /* TODO: ??? */
default:
LOG_WARN("unrecognized mouse button: %d (0x%x)", button, button);
return -1;
}
}
static int
encode_xbutton(int xbutton)
{
switch (xbutton) {
case 1: case 2: case 3:
return xbutton - 1;
case 4: case 5:
/* Like button 1 and 2, but with 64 added */
return xbutton - 4 + 64;
case 6: case 7:
/* Same as 4 and 5. Note: the offset should be something else? */
return xbutton - 6 + 64;
case 8: case 9: case 10: case 11:
/* Similar to 4 and 5, but adding 128 instead of 64 */
return xbutton - 8 + 128;
default:
LOG_ERR("cannot encode X mouse button: %d", xbutton);
return -1;
}
}
static void
report_mouse_click(struct terminal *term, int encoded_button, int row, int col,
bool release)
{
char response[128];
switch (term->mouse_reporting) {
case MOUSE_NORMAL:
snprintf(response, sizeof(response), "\033[M%c%c%c",
32 + (release ? 3 : encoded_button), 32 + col + 1, 32 + row + 1);
break;
case MOUSE_SGR:
snprintf(response, sizeof(response), "\033[<%d;%d;%d%c",
encoded_button, col + 1, row + 1, release ? 'm' : 'M');
break;
case MOUSE_URXVT:
snprintf(response, sizeof(response), "\033[%d;%d;%dM",
32 + (release ? 3 : encoded_button), col + 1, row + 1);
break;
case MOUSE_UTF8:
/* Unimplemented */
return;
}
write(term->ptmx, response, strlen(response));
}
static void
report_mouse_motion(struct terminal *term, int encoded_button, int row, int col)
{
report_mouse_click(term, encoded_button, row, col, false);
}
void
term_mouse_down(struct terminal *term, int button, int row, int col,
bool shift, bool alt, bool ctrl)
{
/* Map libevent button event code to X button number */
int xbutton = linux_mouse_button_to_x(button);
if (xbutton == -1)
return;
int encoded = encode_xbutton(xbutton);
if (encoded == -1)
return;
encoded += (shift ? 4 : 0) + (alt ? 8 : 0) + (ctrl ? 16 : 0);
switch (term->mouse_tracking) {
case MOUSE_NONE:
break;
case MOUSE_X10:
case MOUSE_CLICK:
case MOUSE_DRAG:
case MOUSE_MOTION:
report_mouse_click(term, encoded, row, col, false);
break;
}
}
void
term_mouse_up(struct terminal *term, int button, int row, int col,
bool shift, bool alt, bool ctrl)
{
/* Map libevent button event code to X button number */
int xbutton = linux_mouse_button_to_x(button);
if (xbutton == -1)
return;
if (xbutton == 4 || xbutton == 5) {
/* No release events for scroll buttons */
return;
}
int encoded = encode_xbutton(xbutton);
if (encoded == -1)
return;
encoded += (shift ? 4 : 0) + (alt ? 8 : 0) + (ctrl ? 16 : 0);
switch (term->mouse_tracking) {
case MOUSE_NONE:
break;
case MOUSE_X10:
case MOUSE_CLICK:
case MOUSE_DRAG:
case MOUSE_MOTION:
report_mouse_click(term, encoded, row, col, true);
break;
}
}
void
term_mouse_motion(struct terminal *term, int button, int row, int col,
bool shift, bool alt, bool ctrl)
{
int encoded = 0;
if (button != 0) {
/* Map libevent button event code to X button number */
int xbutton = linux_mouse_button_to_x(button);
if (xbutton == -1)
return;
encoded = encode_xbutton(xbutton);
if (encoded == -1)
return;
} else
encoded = 3; /* "released" */
encoded += 32; /* Motion event */
encoded += (shift ? 4 : 0) + (alt ? 8 : 0) + (ctrl ? 16 : 0);
switch (term->mouse_tracking) {
case MOUSE_NONE:
case MOUSE_X10:
case MOUSE_CLICK:
return;
case MOUSE_DRAG:
if (button == 0)
return;
/* FALLTHROUGH */
case MOUSE_MOTION:
report_mouse_motion(term, encoded, row, col);
break;
}
}