terminal.c (40925B)
1 /* See LICENSE for copyright details */ 2 3 /* TODO: build own wide char tables */ 4 i32 wcwidth(u32 cp); 5 6 static const u8 utf8overhangmask[32] = { 7 255, 255, 255, 255, 255, 255, 255, 255, 8 255, 255, 255, 255, 255, 255, 255, 255, 9 0, 0, 0, 0, 0, 0, 0, 0, 10 0, 0, 0, 0, 0, 0, 0, 0 11 }; 12 13 #define SPLIT_LONG 4096L 14 15 static iv2 16 initialize_framebuffer(Framebuffer *fb, iv2 term_size) 17 { 18 size cell_memory_size = sizeof(Cell) * term_size.h * term_size.w; 19 size rows_memory_size = sizeof(Row) * term_size.h; 20 21 /* NOTE: make sure cell memory size is a multiple of pointer size */ 22 cell_memory_size += (sizeof(void *) - cell_memory_size % sizeof(void *)); 23 24 if (cell_memory_size + rows_memory_size >= fb->backing_store.size) 25 term_size.h = (fb->backing_store.size - cell_memory_size) / sizeof(Row); 26 27 fb->cells = fb->backing_store.memory; 28 fb->rows = fb->backing_store.memory + cell_memory_size; 29 30 for (i32 i = 0; i < term_size.h; i++) 31 fb->rows[i] = fb->cells + i * term_size.w; 32 33 return term_size; 34 } 35 36 static Range 37 get_word_around_cell(Term *t, iv2 cell) 38 { 39 Range result = {.start = cell, .end = cell}; 40 Cell *row = t->views[t->view_idx].fb.rows[cell.y]; 41 42 b32 isspace = ISSPACE(row[cell.x].cp) && !(row[cell.x].bg & ATTR_WDUMMY); 43 while (result.start.x > 0) { 44 Cell *nc = row + result.start.x - 1; 45 if (!(nc->bg & ATTR_WDUMMY) && isspace != ISSPACE(nc->cp)) 46 break; 47 result.start.x--; 48 } 49 while (result.end.x < t->size.w - 1) { 50 Cell *nc = row + result.end.x + 1; 51 if (!(nc->bg & ATTR_WDUMMY) && isspace != ISSPACE(nc->cp)) 52 break; 53 result.end.x++; 54 } 55 56 return result; 57 } 58 59 static Range 60 get_char_around_cell(Term *t, iv2 cell) 61 { 62 Range result = {.start = cell, .end = cell}; 63 64 /* NOTE: if the cell is WDUMMY move back until we find the starting 65 * WIDE cell. then set the range end to the last WDUMMY cell */ 66 Cell *row = t->views[t->view_idx].fb.rows[cell.y]; 67 if (row[cell.x].bg & ATTR_WDUMMY) { 68 ASSERT(cell.x - 1 >= 0); 69 cell.x--; 70 ASSERT(row[cell.x].bg & ATTR_WIDE); 71 result.start = cell; 72 } else if (row[cell.x].bg & ATTR_WIDE) { 73 ASSERT(cell.x + 1 < t->size.w); 74 cell.x++; 75 ASSERT(row[cell.x].bg & ATTR_WDUMMY); 76 result.end = cell; 77 } 78 79 return result; 80 } 81 82 static s8 83 consume(s8 raw, size count) 84 { 85 raw.data += count; 86 raw.len -= count; 87 return raw; 88 } 89 90 static u8 91 peek(s8 raw, size i) 92 { 93 ASSERT(i < raw.len); 94 return raw.data[i]; 95 } 96 97 static u32 98 get_utf8(s8 *raw) 99 { 100 u32 state = 0, cp; 101 size off = 0; 102 while (off < raw->len) { 103 if (!utf8_decode(&state, &cp, raw->data[off++])) { 104 *raw = consume(*raw, off); 105 return cp; 106 } 107 } 108 return (u32)-1; 109 } 110 111 static u32 112 get_ascii(s8 *raw) 113 { 114 ASSERT(raw->len > 0); 115 u32 result = raw->data[0]; 116 *raw = consume(*raw, 1); 117 return result; 118 } 119 120 static size 121 line_length(Line *l) 122 { 123 ASSERT(l->start <= l->end); 124 return l->end - l->start; 125 } 126 127 static s8 128 line_to_s8(Line *l, RingBuf *rb) 129 { 130 ASSERT(l->start <= l->end); 131 132 s8 result = {.len = l->end - l->start, .data = l->start}; 133 return result; 134 } 135 136 static void 137 init_line(Line *l, u8 *position, CellStyle cursor_state) 138 { 139 l->start = position; 140 l->end = position; 141 l->has_unicode = 0; 142 l->cursor_state = cursor_state; 143 } 144 145 static size 146 feed_line(LineBuf *lb, u8 *position, CellStyle cursor_state) 147 { 148 size result = 0; 149 if (lb->buf[lb->widx].start != position) { 150 lb->buf[lb->widx++].end = position; 151 lb->widx = lb->widx >= lb->cap ? 0 : lb->widx; 152 lb->filled += lb->filled <= lb->widx; 153 init_line(lb->buf + lb->widx, position, cursor_state); 154 result = 1; 155 } 156 return result; 157 } 158 159 static void 160 selection_clear(Selection *s) 161 { 162 s->range.end = INVALID_RANGE_END; 163 s->state = SS_NONE; 164 } 165 166 static void 167 selection_scroll(Term *t, i32 origin, i32 n) 168 { 169 Selection *s = &t->selection; 170 if (!is_valid_range(s->range)) 171 return; 172 173 b32 start_in_bounds = BETWEEN(s->range.start.y, origin, t->bot); 174 b32 end_in_bounds = BETWEEN(s->range.end.y, origin, t->bot); 175 if (start_in_bounds != end_in_bounds) { 176 selection_clear(s); 177 } else if (start_in_bounds) { 178 s->range.start.y += n; 179 s->range.end.y += n; 180 if (s->range.start.y > t->bot || s->range.start.y < t->top || 181 s->range.end.y > t->bot || s->range.end.y < t->top) 182 selection_clear(s); 183 } 184 } 185 186 static b32 187 is_selected(Selection *s, i32 x, i32 y) 188 { 189 if (!is_valid_range(s->range)) 190 return 0; 191 192 b32 result = BETWEEN(y, s->range.start.y, s->range.end.y) && 193 (y != s->range.start.y || x >= s->range.start.x) && 194 (y != s->range.end.y || x <= s->range.end.x); 195 return result; 196 } 197 198 static b32 199 selection_intersects_region(Selection *s, iv2 tl, iv2 br) 200 { 201 /* TODO: maybe this can be further simplified (eg. with a k-map) */ 202 Range r = s->range; 203 b32 valid = is_valid_range(r); 204 b32 whole = r.start.y < tl.y && r.end.y > br.y; 205 b32 start_x = r.start.x >= tl.x && r.start.x <= br.x; 206 b32 start_y = r.start.y >= tl.y && r.start.y <= br.y; 207 b32 end_x = r.end.x >= tl.x && r.end.x <= br.x; 208 b32 end_y = r.end.y >= tl.y && r.end.y <= br.y; 209 b32 result = valid && (whole || (start_y && start_x) || (end_y && end_x)); 210 return result; 211 } 212 213 static void 214 fb_clear_region(Term *t, i32 r1, i32 r2, i32 c1, i32 c2) 215 { 216 BEGIN_TIMED_BLOCK(); 217 u32 tmp; 218 if (r1 > r2) { 219 tmp = r1; 220 r1 = r2; 221 r2 = tmp; 222 } 223 if (c1 > c2) { 224 tmp = c1; 225 c1 = c2; 226 c2 = tmp; 227 } 228 CLAMP(c1, 0, t->size.w - 1); 229 CLAMP(c2, 0, t->size.w - 1); 230 CLAMP(r1, 0, t->size.h - 1); 231 CLAMP(r2, 0, t->size.h - 1); 232 233 iv2 top_left = {.x = c1, .y = r1}; 234 iv2 bottom_right = {.x = c2, .y = r2}; 235 if (selection_intersects_region(&t->selection, top_left, bottom_right)) 236 selection_clear(&t->selection); 237 238 Row *rows = t->views[t->view_idx].fb.rows; 239 CellStyle cursor_style = t->cursor.style; 240 u32 fg = SHADER_PACK_FG(cursor_style.fg.rgba, cursor_style.attr); 241 u32 bg = SHADER_PACK_BG(cursor_style.bg.rgba, cursor_style.attr); 242 for (i32 r = top_left.y; r <= bottom_right.y; r++) { 243 for (i32 c = top_left.x; c <= bottom_right.x; c++) { 244 rows[r][c].fg = fg; 245 rows[r][c].bg = bg; 246 rows[r][c].cp = ' '; 247 } 248 } 249 250 END_TIMED_BLOCK(); 251 } 252 253 static void 254 fb_scroll_down(Term *t, i32 top, i32 n) 255 { 256 BEGIN_TIMED_BLOCK(); 257 if (!BETWEEN(top, t->top, t->bot)) 258 goto end; 259 260 TermView *tv = t->views + t->view_idx; 261 CLAMP(n, 0, t->bot - top); 262 263 fb_clear_region(t, t->bot - n + 1, t->bot, 0, t->size.w); 264 for (i32 i = t->bot; i >= top + n; i--) { 265 Row tmp = tv->fb.rows[i]; 266 tv->fb.rows[i] = tv->fb.rows[i - n]; 267 tv->fb.rows[i - n] = tmp; 268 } 269 selection_scroll(t, top, n); 270 end: 271 END_TIMED_BLOCK(); 272 } 273 274 static void 275 fb_scroll_up(Term *t, i32 top, i32 n) 276 { 277 BEGIN_TIMED_BLOCK(); 278 if (!BETWEEN(top, t->top, t->bot)) 279 goto end; 280 281 TermView *tv = t->views + t->view_idx; 282 CLAMP(n, 0, t->bot - top); 283 284 #if 0 285 size cell_count = (t->bot - n + 1) * t->size.w; 286 ASSERT(cell_count < tv->fb.cells_count); 287 mem_move(tv->fb.cells + t->size.w * n, tv->fb.cells, cell_count * sizeof(Cell)); 288 fb_clear_region(t, t->bot - n + 1, t->bot, 0, t->size.w); 289 #else 290 fb_clear_region(t, top, top + n - 1, 0, t->size.w); 291 for (i32 i = top; i <= t->bot - n; i++) { 292 Row tmp = tv->fb.rows[i]; 293 tv->fb.rows[i] = tv->fb.rows[i + n]; 294 tv->fb.rows[i + n] = tmp; 295 } 296 #endif 297 selection_scroll(t, top, -n); 298 end: 299 END_TIMED_BLOCK(); 300 } 301 302 static void 303 swap_screen(Term *t) 304 { 305 t->mode.term ^= TM_ALTSCREEN; 306 t->view_idx = !!(t->mode.term & TM_ALTSCREEN); 307 } 308 309 static void 310 cursor_reset(Term *t) 311 { 312 //(Colour){.rgba = 0x1e9e33ff}; 313 t->cursor.style.fg = g_colours.data[g_colours.fgidx]; 314 t->cursor.style.bg = g_colours.data[g_colours.bgidx]; 315 t->cursor.style.attr = ATTR_NULL; 316 } 317 318 static void 319 cursor_move_to(Term *t, i32 row, i32 col) 320 { 321 i32 minr = 0, maxr = t->size.h - 1; 322 if (t->cursor.state & CURSOR_ORIGIN) { 323 minr = t->top; 324 maxr = t->bot; 325 } 326 t->cursor.pos.y = CLAMP(row, minr, maxr); 327 t->cursor.pos.x = CLAMP(col, 0, t->size.w - 1); 328 t->cursor.state &= ~CURSOR_WRAP_NEXT; 329 } 330 331 static void 332 cursor_move_abs_to(Term *t, i32 row, i32 col) 333 { 334 if (t->cursor.state & CURSOR_ORIGIN) 335 row += t->top; 336 cursor_move_to(t, row, col); 337 } 338 339 static void 340 cursor_alt(Term *t, b32 save) 341 { 342 i32 mode = t->view_idx; 343 if (save) { 344 t->saved_cursors[mode] = t->cursor; 345 } else { 346 t->cursor = t->saved_cursors[mode]; 347 cursor_move_to(t, t->cursor.pos.y, t->cursor.pos.x); 348 } 349 } 350 351 /* NOTE: advance the cursor by <n> cells; handles reverse movement */ 352 static void 353 cursor_step_column(Term *t, i32 n) 354 { 355 i32 col = t->cursor.pos.x + n; 356 i32 row = t->cursor.pos.y; 357 if (col >= t->size.w) { 358 row++; 359 col = 0; 360 if (row >= t->size.h) 361 fb_scroll_up(t, t->top, 1); 362 } 363 cursor_move_to(t, row, col); 364 } 365 366 /* NOTE: steps the cursor without causing a scroll */ 367 static void 368 cursor_step_raw(Term *t, i32 step, i32 rows, i32 cols) 369 { 370 rows *= step; 371 cols *= step; 372 rows = MIN(rows, I32_MAX - t->cursor.pos.y); 373 cols = MIN(cols, I32_MAX - t->cursor.pos.x); 374 cursor_move_to(t, t->cursor.pos.y + rows, t->cursor.pos.x + cols); 375 } 376 377 static void 378 cursor_up(Term *t, i32 requested_count) 379 { 380 i32 cursor_y = t->cursor.pos.y; 381 i32 max = cursor_y >= t->top ? (cursor_y - t->top) : cursor_y; 382 i32 count = MIN(max, MAX(requested_count, 1)); 383 cursor_move_to(t, t->cursor.pos.y - count, t->cursor.pos.x); 384 } 385 386 static void 387 cursor_down(Term *t, i32 requested_count) 388 { 389 i32 cursor_y = t->cursor.pos.y; 390 i32 max = cursor_y <= t->bot ? (t->bot - cursor_y) : (t->size.h - cursor_y - 1); 391 i32 count = MIN(max, MAX(requested_count, 1)); 392 cursor_move_to(t, t->cursor.pos.y + count, t->cursor.pos.x); 393 } 394 395 static i32 396 next_tab_position(Term *t, b32 backwards) 397 { 398 static_assert(ARRAY_COUNT(t->tabs) == 8 * sizeof(*t->tabs), 399 "Term.tabs must be same length as the bitwidth of it's type"); 400 u32 col = t->cursor.pos.x; 401 u32 idx = col / ARRAY_COUNT(t->tabs); 402 u32 bit = col % ARRAY_COUNT(t->tabs); 403 u32 mask = safe_left_shift(1, bit - backwards) - 1; 404 405 i32 result = 32 * idx; 406 if (backwards) { 407 u32 zeroes = clz_u32(t->tabs[idx--] & mask); 408 while (idx < ARRAY_COUNT(t->tabs) && zeroes == 32) 409 zeroes = clz_u32(t->tabs[idx--]); 410 result = 32 * (idx + 1) + 32 - zeroes; 411 result *= col != 0; 412 } else { 413 u32 zeroes = ctz_u32(t->tabs[idx++] & ~mask); 414 while (idx < ARRAY_COUNT(t->tabs) && zeroes == 32) 415 zeroes = ctz_u32(t->tabs[idx++]); 416 result = 32 * (idx - 1) + zeroes + 1; 417 } 418 /* TODO(rnp): is clamping this correct? */ 419 //ASSERT(result < t->size.w); 420 result = MIN(result, t->size.w - 1); 421 422 return result; 423 } 424 425 static void 426 term_tab_col(Term *t, i32 col, b32 set) 427 { 428 ASSERT(col < t->size.w); 429 u32 idx = (col > 0) ? ((col - 1) / ARRAY_COUNT(t->tabs)) : 0; 430 u32 bit = (col > 0) ? ((col - 1) % ARRAY_COUNT(t->tabs)) : 0; 431 u32 mask = 1u; 432 if (bit) mask = safe_left_shift(1, bit); 433 if (set) t->tabs[idx] |= mask; 434 else t->tabs[idx] &= ~mask; 435 } 436 437 static void 438 reset_tabs(Term *t, u32 column_step) 439 { 440 for (i32 i = 0; i < ARRAY_COUNT(t->tabs); i++) 441 t->tabs[i] = 0; 442 for (i32 i = column_step; i < t->size.w; i += column_step) 443 term_tab_col(t, i, 1); 444 } 445 446 static void 447 term_reset(Term *t) 448 { 449 i32 mode = t->mode.term & TM_ALTSCREEN; 450 t->cursor.state = CURSOR_NORMAL; 451 for (u32 i = 0; i < ARRAY_COUNT(t->saved_cursors); i++) { 452 cursor_reset(t); 453 t->cursor.charset_index = 0; 454 for (u32 i = 0; i < ARRAY_COUNT(t->cursor.charsets); i++) 455 t->cursor.charsets[i] = CS_USA; 456 cursor_move_to(t, 0, 0); 457 cursor_alt(t, 1); 458 swap_screen(t); 459 fb_clear_region(t, 0, t->size.h, 0, t->size.w); 460 } 461 reset_tabs(t, g_tabstop); 462 463 t->top = 0; 464 t->bot = t->size.h - 1; 465 /* TODO: why is term_reset() being called when we are in the altscreen */ 466 t->mode.term = mode|TM_AUTO_WRAP|TM_UTF8; 467 } 468 469 static void 470 stream_push_csi(Stream *s, CSI *csi) 471 { 472 stream_push_s8(s, s8("ESC [")); 473 for (size i = 0; i < csi->raw.len; i++) { 474 u8 c = csi->raw.data[i]; 475 if (ISPRINT(c)) { 476 stream_push_byte(s, csi->raw.data[i]); 477 } else if (c == '\n') { 478 stream_push_s8(s, s8("\\n")); 479 } else if (c == '\r') { 480 stream_push_s8(s, s8("\\r")); 481 } else { 482 stream_push_s8(s, s8("\\x")); 483 stream_push_hex_u64(s, c); 484 } 485 } 486 stream_push_s8(s, s8("\n\t{ .priv = ")); 487 stream_push_u64(s, csi->priv); 488 stream_push_s8(s, s8(" .mode = ")); 489 if (ISPRINT(csi->mode)) { 490 stream_push_byte(s, csi->mode); 491 } else { 492 stream_push_s8(s, s8("\\x")); 493 stream_push_hex_u64(s, csi->mode); 494 } 495 496 stream_push_s8(s, s8(", .argc = ")); 497 stream_push_u64(s, csi->argc); 498 stream_push_s8(s, s8(", .argv = {")); 499 for (i32 i = 0; i < csi->argc; i++) { 500 stream_push_byte(s, ' '); 501 stream_push_i64(s, csi->argv[i]); 502 } 503 stream_push_s8(s, s8(" } }\n")); 504 } 505 506 /* ED/DECSED: Erase in Display */ 507 static void 508 erase_in_display(Term *t, CSI *csi) 509 { 510 iv2 cpos = t->cursor.pos; 511 switch (csi->argv[0]) { 512 case 0: /* Erase Below (default) */ 513 fb_clear_region(t, cpos.y, cpos.y, cpos.x, t->size.w); 514 if (cpos.y < t->size.h - 1) 515 fb_clear_region(t, cpos.y + 1, t->size.h, 0, t->size.w); 516 break; 517 case 1: /* Erase Above */ 518 if (cpos.y > 0) 519 fb_clear_region(t, 0, cpos.y - 1, 0, t->size.w); 520 fb_clear_region(t, cpos.y, cpos.y, 0, cpos.x); 521 break; 522 case 2: /* Erase All */ 523 fb_clear_region(t, 0, t->size.h, 0, t->size.w); 524 break; 525 case 3: /* Erase Saved Lines (xterm) */ 526 /* NOTE: ignored; we don't save lines in the way xterm does */ 527 break; 528 default: /* TODO(rnp): warn about invalid argument */ ; 529 } 530 } 531 532 /* EL/DECSEL: Erase in Line */ 533 static void 534 erase_in_line(Term *t, CSI *csi) 535 { 536 iv2 cpos = t->cursor.pos; 537 switch (csi->argv[0]) { 538 case 0: /* Erase to Right */ 539 fb_clear_region(t, cpos.y, cpos.y, cpos.x, t->size.w); 540 break; 541 case 1: /* Erase to Left */ 542 fb_clear_region(t, cpos.y, cpos.y, 0, cpos.x); 543 break; 544 case 2: /* Erase All */ 545 fb_clear_region(t, cpos.y, cpos.y, 0, t->size.w); 546 break; 547 default: /* TODO(rnp): warn about invalid argument */ ; 548 } 549 } 550 551 /* IL: Insert <count> blank lines */ 552 static void 553 insert_blank_lines(Term *t, i32 count) 554 { 555 fb_scroll_down(t, t->cursor.pos.y, count); 556 } 557 558 /* DL: Erase <count> lines */ 559 static void 560 erase_lines(Term *t, i32 count) 561 { 562 fb_scroll_up(t, t->cursor.pos.y, count); 563 } 564 565 /* DCH: Delete <count> Characters */ 566 static void 567 delete_characters(Term *t, i32 requested_count) 568 { 569 iv2 cpos = t->cursor.pos; 570 /* TODO(rnp): if the cursor is outside the left/right margins do nothing */ 571 572 TermView *tv = t->views + t->view_idx; 573 i32 count = MAX(0, MIN(requested_count, t->size.w - cpos.x)); 574 i32 total = t->size.w - count; 575 576 Cell *dst = tv->fb.rows[cpos.y] + cpos.x; 577 if (dst->bg & ATTR_WDUMMY) dst--; 578 Cell *src = dst + count; 579 580 i32 dst_off = 0, src_off = 0; 581 for (i32 i = 0; i < total; i++) { 582 b32 dst_wide = (dst[i].bg & ATTR_WIDE) != 0; 583 b32 src_wide = (src[i].bg & ATTR_WIDE) != 0; 584 585 if (dst_wide && !src_wide) 586 dst[i + 1].bg &= ~ATTR_WDUMMY; 587 588 dst[i + dst_off] = src[i + src_off]; 589 590 if (src_wide && !dst_wide) { 591 dst[i + 1] = src[i + 1]; 592 i++; 593 } 594 } 595 596 fb_clear_region(t, cpos.y, cpos.y, t->size.w - count, t->size.w); 597 } 598 599 /* ECH: Erase <count> Characters */ 600 static void 601 erase_characters(Term *t, i32 count) 602 { 603 iv2 cpos = t->cursor.pos; 604 fb_clear_region(t, cpos.y, cpos.y, cpos.x, cpos.x + count - 1); 605 } 606 607 /* TBC: Tabulation Clear */ 608 static void 609 clear_term_tab(Term *t, i32 arg) 610 { 611 /* TODO: case 1, 2? */ 612 switch(arg) { 613 case 0: 614 term_tab_col(t, t->cursor.pos.x, 0); 615 break; 616 case 3: 617 for (u32 i = 0; i < ARRAY_COUNT(t->tabs); i++) 618 t->tabs[i] = 0; 619 break; 620 default: 621 stream_push_s8(&t->error_stream, s8("clear_term_tab: unhandled arg: ")); 622 stream_push_i64(&t->error_stream, arg); 623 stream_push_byte(&t->error_stream, '\n'); 624 os_write_err_msg(stream_to_s8(&t->error_stream)); 625 t->error_stream.widx = 0; 626 } 627 } 628 629 /* SM/DECSET: Set Mode & RM/DECRST Reset Mode */ 630 static void 631 set_mode(Term *t, CSI *csi, b32 set, ModeState src, ModeState *dest) 632 { 633 BEGIN_TIMED_BLOCK(); 634 i32 alt = t->view_idx; 635 636 /* TODO: this whole thing should be a lookup table */ 637 #define PRIV(a) ((1 << 30) | (a)) 638 for (i32 i = 0; i < csi->argc; i++) { 639 i32 arg = (csi->argv[i]) | ((csi->priv & 1) << 30); 640 switch (arg) { 641 case 4: /* IRM: Insert/Replace Mode */ 642 dest->term &= ~TM_REPLACE; 643 if (set) dest->term |= (src.term & TM_REPLACE); 644 break; 645 case 20: /* LNM: Linefeed Assumes Carriage Return */ 646 dest->term &= ~TM_CRLF; 647 if (set) dest->term |= (src.term & TM_CRLF); 648 break; 649 case PRIV(1): /* DECCKM: use application cursor keys */ 650 dest->win &= ~WM_APPCURSOR; 651 if (set) dest->win |= (src.win & WM_APPCURSOR); 652 break; 653 case PRIV(5): /* DECSCNM: reverse/normal video mode */ 654 dest->win &= ~WM_REVERSE; 655 if (set) dest->win |= (src.win & WM_REVERSE); 656 break; 657 case PRIV(6): /* DECOM: Cursor Origin Mode */ 658 if (set) t->cursor.state |= CURSOR_ORIGIN; 659 else t->cursor.state &= ~CURSOR_ORIGIN; 660 cursor_move_abs_to(t, 0, 0); 661 break; 662 case PRIV(7): /* DECAWM: Auto-Wrap Mode */ 663 dest->term &= ~TM_AUTO_WRAP; 664 if (set) dest->term |= (src.term & TM_AUTO_WRAP); 665 break; 666 case PRIV(1000): /* xterm: report mouse button presses */ 667 dest->win &= ~WM_MOUSE_MASK; 668 if (set) dest->win |= (src.win & WM_MOUSE_BTN); 669 break; 670 case PRIV(1002): /* xterm: cell motion tracking */ 671 dest->win &= ~WM_MOUSE_MASK; 672 if (set) dest->win |= (src.win & WM_MOUSE_TRK); 673 break; 674 case PRIV(1006): /* xterm: SGR mouse mode */ 675 dest->win &= ~WM_MOUSE_SGR; 676 if (set) dest->win |= (src.win & WM_MOUSE_SGR); 677 break; 678 case PRIV(3): /* DECCOLM: 132/80 Column Mode */ 679 case PRIV(4): /* DECSCLM: Fast/Slow Scroll */ 680 case PRIV(8): /* DECARM: Auto-Repeat Keys */ 681 case PRIV(12): /* AT&T 610: Start blinking cursor */ 682 case PRIV(40): /* xterm: (dis)allow 132/80 Column Mode */ 683 case PRIV(45): /* XTREVWRAP: xterm reverse wrap around */ 684 case PRIV(1001): /* xterm: (broken) mouse highlight tracking; requires 685 * the requesting program to be competely functional or will 686 * hang the terminal by design */ 687 case PRIV(1015): /* urxvt: (broken) mouse mode */ 688 /* IGNORED */ 689 break; 690 case PRIV(25): /* DECTCEM: Show/Hide Cursor */ 691 dest->win &= ~WM_HIDECURSOR; 692 if (!set) dest->win |= (src.win & WM_HIDECURSOR); 693 break; 694 case PRIV(1034): /* xterm: enable 8-bit input mode */ 695 dest->win &= ~WM_8BIT; 696 if (set) dest->win |= (src.win & WM_8BIT); 697 break; 698 case PRIV(1049): /* xterm: swap cursor then swap screen */ 699 cursor_alt(t, set); 700 /* FALLTHROUGH */ 701 case PRIV(47): /* xterm: swap screen buffer */ 702 case PRIV(1047): /* xterm: swap screen buffer */ 703 if (alt) fb_clear_region(t, 0, t->size.h, 0, t->size.w); 704 if (set ^ alt) swap_screen(t); 705 if (csi->argv[i] != 1049) break; 706 /* FALLTHROUGH */ 707 case PRIV(1048): /* xterm: swap cursor */ 708 cursor_alt(t, set); 709 break; 710 case PRIV(2004): /* xterm: bracketed paste mode */ 711 dest->win &= ~WM_BRACKPASTE; 712 if (set) dest->win |= (src.win & WM_BRACKPASTE); 713 break; 714 case PRIV(2026): /* synchronized render (eg. wezterm) */ 715 /* IGNORED: we aren't writing some slow garbage so we won't let some 716 * broken program try and stall us because they think we can't operate 717 * fast enough */ 718 break; 719 default: 720 stream_push_s8(&t->error_stream, s8("set_mode: unhandled mode: ")); 721 stream_push_csi(&t->error_stream, csi); 722 os_write_err_msg(stream_to_s8(&t->error_stream)); 723 t->error_stream.widx = 0; 724 } 725 } 726 #undef PRIV 727 END_TIMED_BLOCK(); 728 } 729 730 /* NOTE: adapted from the perl script 256colres.pl in xterm src */ 731 static Colour 732 indexed_colour(i32 index) 733 { 734 Colour result; 735 if (index < 232) { 736 /* NOTE: 16-231 are colours off a 6x6x6 RGB cube */ 737 index -= 16; 738 result.r = 40 * ((index / 36)); 739 result.g = 40 * ((index % 36) / 6); 740 result.b = 40 * ((index % 6)); 741 result.a = 0xFF; 742 if (result.r) result.r += 55; 743 if (result.g) result.g += 55; 744 if (result.b) result.b += 55; 745 } else { 746 /* NOTE: 232-255 are greyscale ramp */ 747 u32 k = (10 * (index - 232) + 8) & 0xFF; 748 result.r = result.g = result.b = k; 749 result.a = 0xFF; 750 } 751 return result; 752 } 753 754 static struct conversion_result 755 direct_colour(i32 *argv, i32 argc, i32 *idx, Stream *err) 756 { 757 struct conversion_result result = {.status = CR_FAILURE}; 758 switch (argv[*idx + 1]) { 759 case 2: /* NOTE: defined RGB colour */ 760 if (*idx + 4 >= argc) { 761 stream_push_s8(err, s8("direct_colour: wrong parameter count: ")); 762 stream_push_u64(err, argc); 763 stream_push_byte(err, '\n'); 764 break; 765 } 766 u32 r = (u32)argv[*idx + 2]; 767 u32 g = (u32)argv[*idx + 3]; 768 u32 b = (u32)argv[*idx + 4]; 769 *idx += 4; 770 if (r > 0xFF || g > 0xFF || b > 0xFF) { 771 stream_push_s8(err, s8("direct_colour: bad rgb colour: (")); 772 stream_push_u64(err, r); 773 stream_push_s8(err, s8(", ")); 774 stream_push_u64(err, g); 775 stream_push_s8(err, s8(", ")); 776 stream_push_u64(err, b); 777 stream_push_s8(err, s8(")\n")); 778 break; 779 } 780 result.colour = (Colour){.r = r, .g = g, .b = b, .a = 0xFF}; 781 result.status = CR_SUCCESS; 782 break; 783 case 5: /* NOTE: indexed colour */ 784 if (*idx + 2 >= argc) { 785 stream_push_s8(err, s8("direct_colour: wrong parameter count: ")); 786 stream_push_u64(err, argc); 787 stream_push_byte(err, '\n'); 788 break; 789 } 790 *idx += 2; 791 if (!BETWEEN(argv[*idx], 0, 255)) { 792 stream_push_s8(err, s8("direct_colour: index parameter out of range: ")); 793 stream_push_i64(err, argv[*idx]); 794 stream_push_byte(err, '\n'); 795 break; 796 } 797 if (BETWEEN(argv[*idx], 0, 16)) 798 result.colour = g_colours.data[argv[*idx]]; 799 else 800 result.colour = indexed_colour(argv[*idx]); 801 result.status = CR_SUCCESS; 802 break; 803 default: 804 stream_push_s8(err, s8("direct_colour: unknown argument: ")); 805 stream_push_i64(err, argv[*idx + 1]); 806 stream_push_byte(err, '\n'); 807 } 808 809 return result; 810 } 811 812 /* SGR: Select Graphic Rendition */ 813 static void 814 set_colours(Term *t, CSI *csi) 815 { 816 BEGIN_TIMED_BLOCK(); 817 CellStyle *cs = &t->cursor.style; 818 struct conversion_result dcr; 819 for (i32 i = 0; i < csi->argc; i++) { 820 switch (csi->argv[i]) { 821 case 0: cursor_reset(t); break; 822 case 1: cs->attr |= ATTR_BOLD; break; 823 case 2: cs->attr |= ATTR_FAINT; break; 824 case 3: cs->attr |= ATTR_ITALIC; break; 825 case 4: cs->attr |= ATTR_UNDERLINED; break; 826 case 5: cs->attr |= ATTR_BLINK; break; 827 case 7: cs->attr |= ATTR_INVERSE; break; 828 case 8: cs->attr |= ATTR_INVISIBLE; break; 829 case 9: cs->attr |= ATTR_STRUCK; break; 830 case 22: cs->attr &= ~(ATTR_BOLD|ATTR_FAINT); break; 831 case 23: cs->attr &= ~ATTR_ITALIC; break; 832 case 24: cs->attr &= ~ATTR_UNDERLINED; break; 833 case 25: cs->attr &= ~ATTR_BLINK; break; 834 case 27: cs->attr &= ~ATTR_INVERSE; break; 835 case 28: cs->attr &= ~ATTR_INVISIBLE; break; 836 case 29: cs->attr &= ~ATTR_STRUCK; break; 837 case 38: 838 dcr = direct_colour(csi->argv, csi->argc, &i, &t->error_stream); 839 if (dcr.status == CR_SUCCESS) { 840 cs->fg = dcr.colour; 841 } else { 842 stream_push_s8(&t->error_stream, s8("set_colours: ")); 843 stream_push_csi(&t->error_stream, csi); 844 os_write_err_msg(stream_to_s8(&t->error_stream)); 845 t->error_stream.widx = 0; 846 } 847 break; 848 849 case 39: cs->fg = g_colours.data[g_colours.fgidx]; break; 850 851 case 48: 852 dcr = direct_colour(csi->argv, csi->argc, &i, &t->error_stream); 853 if (dcr.status == CR_SUCCESS) { 854 cs->bg = dcr.colour; 855 } else { 856 stream_push_s8(&t->error_stream, s8("set_colours: ")); 857 stream_push_csi(&t->error_stream, csi); 858 os_write_err_msg(stream_to_s8(&t->error_stream)); 859 t->error_stream.widx = 0; 860 } 861 break; 862 863 case 49: cs->bg = g_colours.data[g_colours.bgidx]; break; 864 865 default: 866 if (BETWEEN(csi->argv[i], 30, 37)) { 867 cs->fg = g_colours.data[csi->argv[i] - 30]; 868 } else if (BETWEEN(csi->argv[i], 40, 47)) { 869 cs->bg = g_colours.data[csi->argv[i] - 40]; 870 } else if (BETWEEN(csi->argv[i], 90, 97)) { 871 cs->fg = g_colours.data[csi->argv[i] - 82]; 872 } else if (BETWEEN(csi->argv[i], 100, 107)) { 873 cs->bg = g_colours.data[csi->argv[i] - 92]; 874 } else { 875 stream_push_s8(&t->error_stream, s8("unhandled colour arg: ")); 876 stream_push_i64(&t->error_stream, csi->argv[i]); 877 stream_push_byte(&t->error_stream, '\n'); 878 stream_push_csi(&t->error_stream, csi); 879 os_write_err_msg(stream_to_s8(&t->error_stream)); 880 t->error_stream.widx = 0; 881 } 882 } 883 } 884 END_TIMED_BLOCK(); 885 } 886 887 static void 888 set_top_bottom_margins(Term *t, i32 requested_top, i32 requested_bottom) 889 { 890 i32 top = MIN(MAX(1, requested_top), t->size.h); 891 i32 bottom = MIN(t->size.h, (requested_bottom ? requested_bottom : t->size.h)); 892 if (top < bottom) { 893 t->top = top - 1; 894 t->bot = bottom - 1; 895 cursor_move_abs_to(t, 0, 0); 896 } 897 } 898 899 static void 900 window_manipulation(Term *t, CSI *csi) 901 { 902 switch (csi->argv[0]) { 903 case 22: t->platform->get_window_title(&t->saved_title); break; 904 case 23: t->platform->set_window_title(&t->saved_title); break; 905 default: 906 stream_push_s8(&t->error_stream, s8("unhandled xtwinops: ")); 907 stream_push_i64(&t->error_stream, csi->argv[0]); 908 stream_push_byte(&t->error_stream, '\n'); 909 stream_push_csi(&t->error_stream, csi); 910 os_write_err_msg(stream_to_s8(&t->error_stream)); 911 t->error_stream.widx = 0; 912 } 913 } 914 915 static void 916 push_newline(Term *t, b32 move_to_first_col) 917 { 918 i32 row = t->cursor.pos.y; 919 if (row == t->bot && t->scroll_offset == 0) 920 fb_scroll_up(t, t->top, 1); 921 else 922 row++; 923 cursor_move_to(t, row, move_to_first_col? 0 : t->cursor.pos.x); 924 } 925 926 static void 927 push_tab(Term *t, i32 n) 928 { 929 u32 end = ABS(n); 930 for (u32 i = 0; i < end; i++) 931 cursor_move_to(t, t->cursor.pos.y, next_tab_position(t, n < 0)); 932 } 933 934 static b32 935 parse_csi(s8 *r, CSI *csi) 936 { 937 BEGIN_TIMED_BLOCK(); 938 b32 result = 1; 939 940 if (peek(*r, 0) == '?') { 941 csi->priv = 1; 942 get_ascii(r); 943 } 944 945 struct conversion_result arg = {0}; 946 while (r->len) { 947 s8_parse_i32_accum(&arg, *r); 948 *r = arg.unparsed; 949 if (r->len) { 950 u32 cp = get_ascii(r); 951 952 if (ISCONTROL(cp)) 953 continue; 954 955 csi->argv[csi->argc++] = arg.i; 956 zero_struct(&arg); 957 958 if (cp != ';' || csi->argc == ESC_ARG_SIZ) { 959 if (cp == ';') csi->mode = get_ascii(r); 960 else csi->mode = cp; 961 goto end; 962 } 963 } 964 } 965 /* NOTE: if we fell out of the loop then we ran out of characters */ 966 result = 0; 967 end: 968 END_TIMED_BLOCK(); 969 970 return result; 971 } 972 973 static void 974 handle_csi(Term *t, CSI *csi) 975 { 976 BEGIN_TIMED_BLOCK(); 977 s8 raw = csi->raw; 978 b32 ret = parse_csi(&raw, csi); 979 if (!ret) goto unknown; 980 981 #define ORONE(x) ((x)? (x) : 1) 982 983 iv2 p = t->cursor.pos; 984 985 switch (csi->mode) { 986 case 'A': cursor_up(t, csi->argv[0]); break; 987 case 'B': cursor_down(t, csi->argv[0]); break; 988 case 'C': cursor_step_raw(t, ORONE(csi->argv[0]), 0, 1); break; 989 case 'D': cursor_step_raw(t, ORONE(csi->argv[0]), 0, -1); break; 990 case 'E': cursor_move_to(t, p.y + ORONE(csi->argv[0]), 0); break; 991 case 'F': cursor_move_to(t, p.y - ORONE(csi->argv[0]), 0); break; 992 case 'G': cursor_move_to(t, p.y, csi->argv[0] - 1); break; 993 case 'H': cursor_move_abs_to(t, csi->argv[0] - 1, csi->argv[1] - 1); break; 994 case 'J': erase_in_display(t, csi); break; 995 case 'K': erase_in_line(t, csi); break; 996 case 'L': insert_blank_lines(t, ORONE(csi->argv[0])); break; 997 case 'M': erase_lines(t, ORONE(csi->argv[0])); break; 998 case 'P': delete_characters(t, ORONE(csi->argv[0])); break; 999 case 'X': erase_characters(t, ORONE(csi->argv[0])); break; 1000 case 'S': fb_scroll_up(t, t->top, ORONE(csi->argv[0])); break; 1001 case 'T': fb_scroll_down(t, t->top, ORONE(csi->argv[0])); break; 1002 case 'W': if (csi->priv) reset_tabs(t, 8); else goto unknown; break; 1003 case 'Z': push_tab(t, -(ORONE(csi->argv[0]))); break; 1004 case 'a': cursor_step_raw(t, ORONE(csi->argv[0]), 0, 1); break; 1005 case 'd': cursor_move_abs_to(t, csi->argv[0] - 1, p.x); break; 1006 case 'e': cursor_step_raw(t, ORONE(csi->argv[0]), 1, 0); break; 1007 case 'f': cursor_move_abs_to(t, csi->argv[0] - 1, csi->argv[1] - 1); break; 1008 case 'g': clear_term_tab(t, csi->argv[0]); break; 1009 case 'h': set_mode(t, csi, 1, MODE_STATE_ALL_MASK, &t->mode); break; 1010 case 'l': set_mode(t, csi, 0, MODE_STATE_ALL_MASK, &t->mode); break; 1011 case 'm': set_colours(t, csi); break; 1012 case 'n': { 1013 switch (csi->argv[0]) { 1014 case 5: /* NOTE: DSR V-1: Operating Status */ 1015 t->platform->write(t->child, s8("\x1B[0n")); 1016 break; 1017 case 6: /* NOTE: DSR V-2: Cursor Position */ { 1018 iv2 cpos = t->cursor.pos; 1019 if (t->cursor.state & CURSOR_ORIGIN) { 1020 /* TODO(rnp): left/right margin */ 1021 //cpos.x = MAX(0, cpos.x - t->margin.left); 1022 cpos.y = MAX(0, cpos.y - t->top); 1023 } 1024 Stream buf = arena_stream(t->arena_for_frame); 1025 stream_push_s8(&buf, s8("\x1B[")); 1026 stream_push_i64(&buf, cpos.y + 1); 1027 stream_push_byte(&buf, ';'); 1028 stream_push_i64(&buf, cpos.x + 1); 1029 stream_push_byte(&buf, 'R'); 1030 t->platform->write(t->child, stream_to_s8(&buf)); 1031 } break; 1032 default: goto unknown; 1033 } 1034 } break; 1035 case 'r': 1036 if (csi->priv) { 1037 /* NOTE: XTRESTORE: restore the value of a private mode */ 1038 set_mode(t, csi, 1, t->saved_mode, &t->mode); 1039 } else { 1040 set_top_bottom_margins(t, csi->argv[0], csi->argv[1]); 1041 } 1042 break; 1043 case 's': 1044 if (csi->priv) { 1045 /* NOTE: XTSAVE: save the value of a private mode */ 1046 set_mode(t, csi, 1, t->mode, &t->saved_mode); 1047 } else if (csi->argc == 1) { 1048 /* NOTE: SCOSC/ANSI.SYS: save the cursor */ 1049 cursor_alt(t, 1); 1050 } else { 1051 goto unknown; 1052 } 1053 break; 1054 case 't': window_manipulation(t, csi); break; 1055 case '!': 1056 if (csi->raw.data[csi->raw.len - 1] == 'p') { 1057 /* NOTE: DECSTR: soft terminal reset IGNORED */ 1058 break; 1059 } 1060 goto unknown; 1061 case '"': 1062 if (csi->raw.data[csi->raw.len - 1] == 'p') { 1063 /* TODO: we should look at the second parameter and 1064 * use it modify C1 control character mode */ 1065 /* NOTE: DECSCL: set conformance level IGNORED */ 1066 break; 1067 } 1068 goto unknown; 1069 default: 1070 unknown: 1071 stream_push_s8(&t->error_stream, s8("unknown csi: ")); 1072 stream_push_csi(&t->error_stream, csi); 1073 os_write_err_msg(stream_to_s8(&t->error_stream)); 1074 t->error_stream.widx = 0; 1075 } 1076 END_TIMED_BLOCK(); 1077 } 1078 1079 static b32 1080 parse_osc(s8 *raw, OSC *osc) 1081 { 1082 BEGIN_TIMED_BLOCK(); 1083 1084 b32 result = 0; 1085 /* TODO(rnp): make this whole function re-entrant */ 1086 zero_struct(osc); 1087 osc->raw.data = raw->data; 1088 1089 struct conversion_result cmd = s8_parse_i32_until(*raw, ';'); 1090 if (cmd.status != CR_FAILURE) { 1091 osc->cmd = cmd.i; 1092 osc->arg = cmd.unparsed; 1093 osc->raw.len = osc->arg.data - raw->data; 1094 *raw = consume(*raw, osc->raw.len); 1095 } 1096 1097 if (osc->arg.len && peek(osc->arg, 0) == ';') { 1098 osc->arg.data++; 1099 osc->arg.len = 0; 1100 osc->raw.len++; 1101 get_ascii(raw); 1102 while (raw->len) { 1103 u32 cp = get_ascii(raw); 1104 osc->raw.len++; 1105 if (cp == '\a') { 1106 result = 1; 1107 break; 1108 } 1109 if (cp == 0x1B && (raw->len && peek(*raw, 0) == '\\')) { 1110 get_ascii(raw); 1111 osc->raw.len++; 1112 result = 1; 1113 break; 1114 } 1115 osc->arg.len++; 1116 } 1117 } 1118 1119 END_TIMED_BLOCK(); 1120 1121 return result; 1122 } 1123 1124 static void 1125 reset_csi(CSI *csi, s8 *raw) 1126 { 1127 *csi = (CSI){0}; 1128 csi->raw.data = raw->data; 1129 } 1130 1131 static void 1132 dump_osc(OSC *osc, Stream *err) 1133 { 1134 stream_push_s8(err, s8("ESC]")); 1135 for (size i = 0; i < osc->raw.len; i++) { 1136 u8 cp = osc->raw.data[i]; 1137 if (ISPRINT(cp)) { 1138 stream_push_byte(err, cp); 1139 } else if (cp == '\n') { 1140 stream_push_s8(err, s8("\\n")); 1141 } else if (cp == '\r') { 1142 stream_push_s8(err, s8("\\r")); 1143 } else if (cp == '\a') { 1144 stream_push_s8(err, s8("\\a")); 1145 } else { 1146 stream_push_s8(err, s8("\\x")); 1147 stream_push_hex_u64(err, cp); 1148 } 1149 } 1150 stream_push_s8(err, s8("\n\t.cmd = ")); 1151 stream_push_u64(err, osc->cmd); 1152 stream_push_s8(err, s8(", .arg = {.len = ")); 1153 stream_push_i64(err, osc->arg.len); 1154 stream_push_s8(err, s8("}\n")); 1155 os_write_err_msg(stream_to_s8(err)); 1156 err->widx = 0; 1157 } 1158 1159 static void 1160 handle_osc(Term *t, s8 *raw, Arena a) 1161 { 1162 BEGIN_TIMED_BLOCK(); 1163 OSC osc; 1164 if (!parse_osc(raw, &osc)) 1165 goto unknown; 1166 1167 Stream buffer = arena_stream(a); 1168 switch (osc.cmd) { 1169 case 0: stream_push_s8(&buffer, osc.arg); t->platform->set_window_title(&buffer); break; 1170 case 1: break; /* IGNORED: set icon name */ 1171 case 2: stream_push_s8(&buffer, osc.arg); t->platform->set_window_title(&buffer); break; 1172 default: 1173 unknown: 1174 stream_push_s8(&t->error_stream, s8("unhandled osc cmd: ")); 1175 dump_osc(&osc, &t->error_stream); 1176 break; 1177 } 1178 END_TIMED_BLOCK(); 1179 } 1180 1181 static i32 1182 handle_escape(Term *t, s8 *raw, Arena a) 1183 { 1184 BEGIN_TIMED_BLOCK(); 1185 i32 result = 0; 1186 u32 cp = get_ascii(raw); 1187 switch (cp) { 1188 case '[': reset_csi(&t->csi, raw); t->escape |= EM_CSI; break; 1189 case ']': handle_osc(t, raw, a); break; 1190 case '%': /* utf-8 mode */ 1191 /* TODO: should this really be done here? */ 1192 if (!raw->len) { 1193 result = 1; 1194 } else { 1195 switch (get_ascii(raw)) { 1196 case 'G': t->mode.term |= TM_UTF8; break; 1197 case '@': t->mode.term &= ~TM_UTF8; break; 1198 } 1199 } 1200 break; 1201 case '(': /* GZD4 -- set primary charset G0 */ 1202 case ')': /* G1D4 -- set secondary charset G1 */ 1203 case '*': /* G2D4 -- set tertiary charset G2 */ 1204 case '+': { /* G3D4 -- set quaternary charset G3 */ 1205 i32 index = cp - '('; 1206 /* TODO: should this really be done here? */ 1207 if (!raw->len) { 1208 result = 1; 1209 } else { 1210 u32 cs = get_ascii(raw); 1211 switch (cs) { 1212 case '0': t->cursor.charsets[index] = CS_GRAPHIC0; break; 1213 case 'B': t->cursor.charsets[index] = CS_USA; break; 1214 default: 1215 stream_push_s8(&t->error_stream, s8("unhandled charset: ")); 1216 stream_push_byte(&t->error_stream, cs); 1217 stream_push_byte(&t->error_stream, '\n'); 1218 os_write_err_msg(stream_to_s8(&t->error_stream)); 1219 t->error_stream.widx = 0; 1220 break; 1221 } 1222 } 1223 } break; 1224 case '=': /* DECPAM -- application keypad */ 1225 case '>': /* DECPNM -- normal keypad mode */ 1226 /* TODO: MODE_APPKEYPAD */ 1227 break; 1228 case 'c': /* RIS -- Reset to Initial State */ 1229 term_reset(t); 1230 break; 1231 case 'D': /* IND -- Linefeed */ 1232 push_newline(t, 0); 1233 break; 1234 case 'E': /* NEL -- Next Line */ 1235 push_newline(t, 1); 1236 break; 1237 case 'H': /* HTS -- Horizontal Tab Stop */ 1238 term_tab_col(t, t->cursor.pos.x, 1); 1239 break; 1240 case 'M': /* RI -- Reverse Index */ 1241 if (t->cursor.pos.y == t->top) { 1242 fb_scroll_down(t, t->top, 1); 1243 } else { 1244 cursor_move_to(t, t->cursor.pos.y - 1, t->cursor.pos.x); 1245 } 1246 break; 1247 case '7': /* DECSC: Save Cursor */ 1248 cursor_alt(t, 1); 1249 break; 1250 case '8': /* DECRC: Restore Cursor */ 1251 cursor_alt(t, 0); 1252 break; 1253 default: 1254 stream_push_s8(&t->error_stream, s8("unknown escape sequence: ESC ")); 1255 stream_push_byte(&t->error_stream, cp); 1256 stream_push_s8(&t->error_stream, s8(" (0x")); 1257 stream_push_hex_u64(&t->error_stream, cp); 1258 stream_push_s8(&t->error_stream, s8(")\n")); 1259 os_write_err_msg(stream_to_s8(&t->error_stream)); 1260 t->error_stream.widx = 0; 1261 break; 1262 } 1263 END_TIMED_BLOCK(); 1264 return result; 1265 } 1266 1267 static i32 1268 push_control(Term *t, s8 *line, u32 cp, Arena a) 1269 { 1270 i32 result = 0; 1271 switch (cp) { 1272 case 0x1B: 1273 if (!line->len) result = 1; 1274 else result = handle_escape(t, line, a); 1275 break; 1276 case '\r': cursor_move_to(t, t->cursor.pos.y, 0); break; 1277 case '\n': push_newline(t, t->mode.term & TM_CRLF); break; 1278 case '\t': push_tab(t, 1); break; 1279 case '\a': /* TODO: ding ding? */ break; 1280 case '\b': 1281 cursor_move_to(t, t->cursor.pos.y, t->cursor.pos.x - 1); 1282 break; 1283 case 0x0E: /* SO (LS1: Locking Shift 1) */ 1284 case 0x0F: /* SI (LS0: Locking Shift 0) */ 1285 t->cursor.charset_index = 1 - (cp - 0x0E); 1286 break; 1287 default: 1288 stream_push_s8(&t->error_stream, s8("unknown control code: 0x")); 1289 stream_push_hex_u64(&t->error_stream, cp); 1290 stream_push_byte(&t->error_stream, '\n'); 1291 os_write_err_msg(stream_to_s8(&t->error_stream)); 1292 t->error_stream.widx = 0; 1293 break; 1294 } 1295 if (cp != 0x1B) { 1296 if (t->escape & EM_CSI) t->csi.raw.len++; 1297 } 1298 return result; 1299 } 1300 1301 static void 1302 push_normal_cp(Term *t, TermView *tv, u32 cp) 1303 { 1304 BEGIN_TIMED_BLOCK(); 1305 1306 if (t->mode.term & TM_AUTO_WRAP && t->cursor.state & CURSOR_WRAP_NEXT) 1307 push_newline(t, 1); 1308 1309 if (t->cursor.charsets[t->cursor.charset_index] == CS_GRAPHIC0 && BETWEEN(cp, 0x60, 0x7e)) 1310 cp = graphic_0[cp - 0x60]; 1311 1312 i32 width = 1; 1313 if (cp > 0x7F) { 1314 width = wcwidth(cp); 1315 ASSERT(width != -1); 1316 } 1317 1318 /* NOTE: make this '>=' for fun in vis */ 1319 if (t->cursor.pos.x + width > t->size.w) { 1320 /* NOTE: make space for character if mode enabled else 1321 * clobber whatever was on the end of the line */ 1322 if (t->mode.term & TM_AUTO_WRAP) 1323 push_newline(t, 1); 1324 else 1325 cursor_move_to(t, t->cursor.pos.y, t->size.w - width); 1326 } 1327 1328 Cell *c = &tv->fb.rows[t->cursor.pos.y][t->cursor.pos.x]; 1329 /* TODO: pack cell into ssbo */ 1330 c->cp = cp; 1331 c->fg = SHADER_PACK_FG(t->cursor.style.fg.rgba, t->cursor.style.attr); 1332 c->bg = SHADER_PACK_BG(t->cursor.style.bg.rgba, t->cursor.style.attr); 1333 1334 if (width > 1) { 1335 ASSERT(t->cursor.pos.x + (width - 1) < t->size.w); 1336 c[0].bg |= ATTR_WIDE; 1337 c[1].bg |= ATTR_WDUMMY; 1338 } 1339 1340 if (t->cursor.pos.x + width < t->size.w) 1341 cursor_step_column(t, width); 1342 else 1343 t->cursor.state |= CURSOR_WRAP_NEXT; 1344 1345 /* TODO: region detection */ 1346 if (is_selected(&t->selection, t->cursor.pos.x, t->cursor.pos.y)) 1347 selection_clear(&t->selection); 1348 1349 END_TIMED_BLOCK(); 1350 } 1351 1352 static void 1353 push_line(Term *t, Line *line, Arena a) 1354 { 1355 BEGIN_TIMED_BLOCK(); 1356 1357 TermView *tv = t->views + t->view_idx; 1358 s8 l = line_to_s8(line, &tv->log); 1359 t->cursor.style = line->cursor_state; 1360 1361 while (l.len) { 1362 u32 cp; 1363 if (line->has_unicode) cp = get_utf8(&l); 1364 else cp = get_ascii(&l); 1365 1366 ASSERT(cp != (u32)-1); 1367 if (ISCONTROL(cp)) { 1368 if (!(t->mode.term & TM_UTF8) || !ISCONTROLC1(cp)) 1369 push_control(t, &l, cp, a); 1370 continue; 1371 } else if (t->escape & EM_CSI) { 1372 t->csi.raw.len++; 1373 if (BETWEEN(cp, '@', '~')) { 1374 handle_csi(t, &t->csi); 1375 t->escape &= ~EM_CSI; 1376 } 1377 continue; 1378 } 1379 1380 push_normal_cp(t, tv, cp); 1381 } 1382 END_TIMED_BLOCK(); 1383 } 1384 1385 static size 1386 get_line_idx(LineBuf *lb, size off) 1387 { 1388 ASSERT(-off <= lb->filled); 1389 size result = lb->widx + off; 1390 if (result < 0) 1391 result += lb->filled; 1392 return result; 1393 } 1394 1395 static void 1396 blit_lines(Term *t, Arena a) 1397 { 1398 BEGIN_TIMED_BLOCK(); 1399 1400 ASSERT(t->gl.flags & NEEDS_REFILL); 1401 term_reset(t); 1402 1403 TermView *tv = t->views + t->view_idx; 1404 size line_count = t->size.h - 1; 1405 size off = t->scroll_offset; 1406 CLAMP(line_count, 0, tv->lines.filled); 1407 for (size idx = -line_count; idx <= 0; idx++) { 1408 size line_idx = get_line_idx(&tv->lines, idx - off); 1409 push_line(t, tv->lines.buf + line_idx, a); 1410 } 1411 1412 t->gl.flags &= ~NEEDS_REFILL; 1413 1414 END_TIMED_BLOCK(); 1415 } 1416 1417 static void 1418 handle_input(Term *t, Arena a, s8 raw) 1419 { 1420 BEGIN_TIMED_BLOCK(); 1421 1422 TermView *tv = t->views + t->view_idx; 1423 1424 /* TODO: SIMD look ahead */ 1425 while (raw.len) { 1426 size start_len = raw.len; 1427 u32 cp = peek(raw, 0); 1428 /* TODO: this could be a performance issue; may need seperate code path for 1429 * terminal when not in UTF8 mode */ 1430 if (cp > 0x7F && (t->mode.term & TM_UTF8)) { 1431 cp = get_utf8(&raw); 1432 if (cp == (u32)-1 && start_len < 4) { 1433 /* NOTE: Need More Bytes! */ 1434 goto end; 1435 } else if (cp == (u32)-1 && start_len >= 4) { 1436 /* NOTE(rnp): invalid/garbage cp; treat as ASCII control char */ 1437 cp = get_ascii(&raw); 1438 } else if (cp != (u32)-1) { 1439 tv->lines.buf[tv->lines.widx].has_unicode = 1; 1440 } 1441 } else { 1442 cp = get_ascii(&raw); 1443 } 1444 1445 ASSERT(cp != (u32)-1); 1446 1447 if (ISCONTROL(cp)) { 1448 if (!(t->mode.term & TM_UTF8) || !ISCONTROLC1(cp)) { 1449 i32 old_curs_y = t->cursor.pos.y; 1450 if (push_control(t, &raw, cp, a)) { 1451 raw.len = start_len; 1452 goto end; 1453 } 1454 if (!t->escape && (cp == '\n' || t->cursor.pos.y != old_curs_y)) 1455 feed_line(&tv->lines, raw.data, t->cursor.style); 1456 } 1457 continue; 1458 } else if (t->escape & EM_CSI) { 1459 t->csi.raw.len++; 1460 if (BETWEEN(cp, '@', '~')) { 1461 i32 old_curs_y = t->cursor.pos.y; 1462 u32 mode = t->mode.term & TM_ALTSCREEN; 1463 handle_csi(t, &t->csi); 1464 t->escape &= ~EM_CSI; 1465 if ((t->mode.term & TM_ALTSCREEN) != mode) { 1466 u8 *old = raw.data - t->csi.raw.len - 2; 1467 ASSERT(*old == 0x1B); 1468 feed_line(&tv->lines, old, t->cursor.style); 1469 TermView *nv = t->views + t->view_idx; 1470 size nstart = nv->log.widx; 1471 mem_copy(raw.data, nv->log.buf + nstart, raw.len); 1472 commit_to_rb(tv, -raw.len); 1473 commit_to_rb(nv, raw.len); 1474 raw.data = nv->log.buf + nstart; 1475 init_line(nv->lines.buf + nv->lines.widx, raw.data, 1476 t->cursor.style); 1477 tv = nv; 1478 } else if (t->cursor.pos.y != old_curs_y) { 1479 feed_line(&tv->lines, raw.data, t->cursor.style); 1480 } 1481 } 1482 continue; 1483 } 1484 1485 push_normal_cp(t, tv, cp); 1486 1487 } 1488 end: 1489 tv->lines.buf[tv->lines.widx].end = raw.data; 1490 1491 /* TODO: this shouldn't be needed */ 1492 if (tv->lines.buf[tv->lines.widx].end < tv->lines.buf[tv->lines.widx].start) 1493 tv->lines.buf[tv->lines.widx].start -= tv->log.cap; 1494 1495 if (!t->escape && line_length(tv->lines.buf + tv->lines.widx) > SPLIT_LONG) 1496 feed_line(&tv->lines, raw.data, t->cursor.style); 1497 1498 t->unprocessed_bytes = raw.len; 1499 END_TIMED_BLOCK(); 1500 }