vt100/src/vt100.c
tx tx_byte last_col clear_cells reset_tabs full_reset vt_init scroll_up scroll_down index_down reverse_index move_to glyph_for put_char erase_display erase_line save_cursor restore_cursor param set_mode sgr csi_dispatch esc_dispatch control vt_receive vt_receive_buf shifted send_key repeats vt_key vt_tick vt_take_tx glyph_dots vt_render vt_text
1/* DEC VT100 terminal. See vt100.h for the sources. Quotes are from the VT100 Technical Manual2 * (EK-VT100-TM-003) and User Guide (EK-VT100-UG) as published on vt100.net. */3#include "vt100.h"45#include <stdio.h>6#include <string.h>78#include "vt100_font.inc"910enum { S_GROUND, S_ESC, S_ESC_INTER, S_CSI, S_CSI_IGNORE };1112/* blink timing, from 60 Hz frames. "The blink rate is about half that of the cursor or about13 * 0.5 Hz": the cursor changes every 32 frames, blinking characters every 64. */14#define CURSOR_HALF_PERIOD_MS (32 * 1000.0 / 60.0)15#define BLINK_HALF_PERIOD_MS (64 * 1000.0 / 60.0)1617/* "the timer decrements to zero. This takes about one-half a second. Then the key is sent to the18 * keyboard buffer a second time ... This time the count lasts about one thirtieth of a second." */19#define REPEAT_DELAY_MS 500.020#define REPEAT_INTERVAL_MS (1000.0 / 30.0)2122static void tx(vt100 *t, const char *s) {23 size_t n = strlen(s);24 if (t->tx_len + (int)n > (int)sizeof t->tx) return; /* keyboard buffer full: keys are lost */25 memcpy(t->tx + t->tx_len, s, n);26 t->tx_len += (int)n;27}2829static void tx_byte(vt100 *t, uint8_t b) {30 if (t->tx_len < (int)sizeof t->tx) t->tx[t->tx_len++] = b;31}3233static int last_col(const vt100 *t, int row) {34 return t->line_attr[row] == VT_LINE_NORMAL ? VT_COLS - 1 : VT_COLS / 2 - 1;35}3637static void clear_cells(vt100 *t, int row, int from, int to) {38 for (int c = from; c <= to; c++) {39 t->cells[row][c].glyph = ' ';40 t->cells[row][c].attr = 0;41 }42 t->dirty = true;43}4445static void reset_tabs(vt100 *t) {46 for (int c = 0; c < VT_COLS; c++) t->tabs[c] = c > 0 && c % 8 == 0;47}4849static void full_reset(vt100 *t) {50 bool click = t->keyclick, block = t->block_cursor;51 uint8_t leds = t->leds & (1 << VT_LED_ONLINE);52 memset(t->cells, 0, sizeof t->cells);53 for (int r = 0; r < VT_ROWS; r++) clear_cells(t, r, 0, VT_COLS - 1);54 memset(t->line_attr, 0, sizeof t->line_attr);55 t->row = t->col = 0;56 t->wrap_pending = false;57 t->attr = 0;58 t->top = 0;59 t->bottom = VT_ROWS - 1;60 reset_tabs(t);61 t->g[0] = t->g[1] = 'B';62 t->gl = 0;63 t->lnm = t->decckm = t->decom = t->decscnm = t->deckpam = false;64 t->decawm = false; /* ASSUMPTION: SET-UP B "auto wrap" off */65 t->decarm = true;66 t->saved.row = t->saved.col = 0;67 t->saved.attr = 0;68 t->saved.origin_mode = false;69 t->saved.g[0] = t->saved.g[1] = 'B';70 t->saved.gl = 0;71 t->state = S_GROUND;72 t->keyclick = click;73 t->block_cursor = block;74 t->leds = leds;75 t->dirty = true;76}7778void vt_init(vt100 *t) {79 memset(t, 0, sizeof *t);80 t->keyclick = true;81 t->block_cursor = true; /* ASSUMPTION: SET-UP "cursor" set to blinking block */82 t->leds = 1 << VT_LED_ONLINE;83 t->last_phase = -1;84 full_reset(t);85}8687/* ---- screen operations ---- */8889static void scroll_up(vt100 *t, int top, int bottom) {90 memmove(&t->cells[top], &t->cells[top + 1], sizeof t->cells[0] * (size_t)(bottom - top));91 memmove(&t->line_attr[top], &t->line_attr[top + 1], (size_t)(bottom - top));92 t->line_attr[bottom] = VT_LINE_NORMAL;93 clear_cells(t, bottom, 0, VT_COLS - 1);94}9596static void scroll_down(vt100 *t, int top, int bottom) {97 memmove(&t->cells[top + 1], &t->cells[top], sizeof t->cells[0] * (size_t)(bottom - top));98 memmove(&t->line_attr[top + 1], &t->line_attr[top], (size_t)(bottom - top));99 t->line_attr[top] = VT_LINE_NORMAL;100 clear_cells(t, top, 0, VT_COLS - 1);101}102103static void index_down(vt100 *t) { /* IND / LF */104 if (t->row == t->bottom)105 scroll_up(t, t->top, t->bottom);106 else if (t->row < VT_ROWS - 1)107 t->row++;108}109110static void reverse_index(vt100 *t) { /* RI */111 if (t->row == t->top)112 scroll_down(t, t->top, t->bottom);113 else if (t->row > 0)114 t->row--;115}116117static void move_to(vt100 *t, int row, int col) {118 int lo = t->decom ? t->top : 0, hi = t->decom ? t->bottom : VT_ROWS - 1;119 row += lo;120 if (row < lo) row = lo;121 if (row > hi) row = hi;122 t->row = row;123 if (col < 0) col = 0;124 if (col > last_col(t, row)) col = last_col(t, row);125 t->col = col;126 t->wrap_pending = false;127 t->dirty = true;128}129130static uint8_t glyph_for(const vt100 *t, uint8_t c) {131 uint8_t set = t->g[t->gl];132 if (set == '0' || set == '2') { /* special graphics: 0x5F-0x7E live at ROM 0x00-0x1F */133 if (c >= 0x5F && c <= 0x7E) return (uint8_t)(c - 0x5F);134 } else if (set == 'A' && c == '#') {135 return 0x1E; /* pound sign */136 }137 return c;138}139140static void put_char(vt100 *t, uint8_t c) {141 if (t->wrap_pending && t->decawm) {142 t->col = 0;143 index_down(t);144 }145 t->wrap_pending = false;146 int lc = last_col(t, t->row);147 if (t->col > lc) t->col = lc;148 t->cells[t->row][t->col].glyph = glyph_for(t, c);149 t->cells[t->row][t->col].attr = t->attr;150 if (t->col == lc)151 t->wrap_pending = true; /* the VT100 keeps the cursor on the last column */152 else153 t->col++;154 t->dirty = true;155}156157static void erase_display(vt100 *t, int mode) {158 if (mode == 0) {159 clear_cells(t, t->row, t->col, VT_COLS - 1);160 for (int r = t->row + 1; r < VT_ROWS; r++) {161 clear_cells(t, r, 0, VT_COLS - 1);162 t->line_attr[r] = VT_LINE_NORMAL;163 }164 } else if (mode == 1) {165 for (int r = 0; r < t->row; r++) {166 clear_cells(t, r, 0, VT_COLS - 1);167 t->line_attr[r] = VT_LINE_NORMAL;168 }169 clear_cells(t, t->row, 0, t->col);170 } else if (mode == 2) {171 for (int r = 0; r < VT_ROWS; r++) {172 clear_cells(t, r, 0, VT_COLS - 1);173 t->line_attr[r] = VT_LINE_NORMAL;174 }175 }176}177178static void erase_line(vt100 *t, int mode) {179 if (mode == 0) clear_cells(t, t->row, t->col, VT_COLS - 1);180 else if (mode == 1) clear_cells(t, t->row, 0, t->col);181 else if (mode == 2) clear_cells(t, t->row, 0, VT_COLS - 1);182}183184static void save_cursor(vt100 *t) {185 t->saved.row = t->row;186 t->saved.col = t->col;187 t->saved.attr = t->attr;188 t->saved.origin_mode = t->decom;189 t->saved.g[0] = t->g[0];190 t->saved.g[1] = t->g[1];191 t->saved.gl = t->gl;192}193194static void restore_cursor(vt100 *t) {195 t->row = t->saved.row;196 t->col = t->saved.col;197 t->attr = t->saved.attr;198 t->decom = t->saved.origin_mode;199 t->g[0] = t->saved.g[0];200 t->g[1] = t->saved.g[1];201 t->gl = t->saved.gl;202 t->wrap_pending = false;203 if (t->col > last_col(t, t->row)) t->col = last_col(t, t->row);204 t->dirty = true;205}206207static int param(const vt100 *t, int i, int dflt) {208 return (i < t->nparams && t->params[i] > 0) ? t->params[i] : dflt;209}210211static void set_mode(vt100 *t, bool on) {212 for (int i = 0; i < (t->nparams ? t->nparams : 1); i++) {213 int p = t->params[i];214 if (!t->private_marker) {215 if (p == 20) t->lnm = on;216 continue;217 }218 switch (p) {219 case 1: t->decckm = on; break;220 case 3: /* DECCOLM: this emulation only has 80 columns, but the side effects stay */221 erase_display(t, 2);222 t->top = 0;223 t->bottom = VT_ROWS - 1;224 move_to(t, 0, 0);225 break;226 case 5: t->decscnm = on; t->dirty = true; break;227 case 6: t->decom = on; move_to(t, 0, 0); break;228 case 7: t->decawm = on; break;229 case 8: t->decarm = on; break;230 default: break; /* 2 (VT52 mode), 4 (smooth scroll), 9 (interlace): not emulated */231 }232 }233}234235static void sgr(vt100 *t) {236 for (int i = 0; i < (t->nparams ? t->nparams : 1); i++) {237 switch (t->params[i]) {238 case 0: t->attr = 0; break;239 case 1: t->attr |= VT_ATTR_BOLD; break;240 case 4: t->attr |= VT_ATTR_UNDERLINE; break;241 case 5: t->attr |= VT_ATTR_BLINK; break;242 case 7: t->attr |= VT_ATTR_REVERSE; break;243 default: break;244 }245 }246}247248static void csi_dispatch(vt100 *t, uint8_t final) {249 char buf[32];250 switch (final) {251 case 'A': move_to(t, t->row - (t->decom ? t->top : 0) - param(t, 0, 1), t->col);252 if (!t->decom && t->row < t->top && t->row + param(t, 0, 1) >= t->top) t->row = t->top;253 break;254 case 'B': {255 int r = t->row + param(t, 0, 1);256 int lim = (t->row <= t->bottom) ? t->bottom : VT_ROWS - 1;257 t->row = r > lim ? lim : r;258 t->wrap_pending = false;259 t->dirty = true;260 break;261 }262 case 'C': t->col += param(t, 0, 1);263 if (t->col > last_col(t, t->row)) t->col = last_col(t, t->row);264 t->wrap_pending = false;265 t->dirty = true;266 break;267 case 'D': t->col -= param(t, 0, 1);268 if (t->col < 0) t->col = 0;269 t->wrap_pending = false;270 t->dirty = true;271 break;272 case 'H':273 case 'f': move_to(t, param(t, 0, 1) - 1, param(t, 1, 1) - 1); break;274 case 'J': erase_display(t, t->nparams ? t->params[0] : 0); break;275 case 'K': erase_line(t, t->nparams ? t->params[0] : 0); break;276 case 'm': sgr(t); break;277 case 'r': {278 int top = param(t, 0, 1) - 1, bottom = param(t, 1, VT_ROWS) - 1;279 if (bottom > VT_ROWS - 1) bottom = VT_ROWS - 1;280 if (top < bottom) {281 t->top = top;282 t->bottom = bottom;283 move_to(t, 0, 0);284 }285 break;286 }287 case 'h': set_mode(t, true); break;288 case 'l': set_mode(t, false); break;289 case 'g':290 if (param(t, 0, 0) == 0) t->tabs[t->col] = false;291 else if (t->params[0] == 3) memset(t->tabs, 0, sizeof t->tabs);292 break;293 case 'q': /* DECLL */294 for (int i = 0; i < (t->nparams ? t->nparams : 1); i++) {295 int p = t->params[i];296 if (p == 0) t->leds &= (uint8_t)~((1 << VT_LED_L1) | (1 << VT_LED_L2) | (1 << VT_LED_L3) | (1 << VT_LED_L4));297 else if (p >= 1 && p <= 4) t->leds |= (uint8_t)(1 << (VT_LED_L1 + p - 1));298 }299 break;300 case 'c': /* DA: a VT100 with the advanced video option */301 if (param(t, 0, 0) == 0) tx(t, "\033[?1;2c");302 break;303 case 'n':304 if (param(t, 0, 0) == 5) {305 tx(t, "\033[0n");306 } else if (param(t, 0, 0) == 6) {307 snprintf(buf, sizeof buf, "\033[%d;%dR", t->row - (t->decom ? t->top : 0) + 1, t->col + 1);308 tx(t, buf);309 }310 break;311 default: break;312 }313}314315static void esc_dispatch(vt100 *t, uint8_t final) {316 if (t->intermediate == '#') {317 int la = -1;318 switch (final) {319 case '3': la = VT_LINE_DOUBLE_TOP; break;320 case '4': la = VT_LINE_DOUBLE_BOTTOM; break;321 case '5': la = VT_LINE_NORMAL; break;322 case '6': la = VT_LINE_DOUBLE_WIDTH; break;323 case '8': /* DECALN */324 for (int r = 0; r < VT_ROWS; r++) {325 t->line_attr[r] = VT_LINE_NORMAL;326 for (int c = 0; c < VT_COLS; c++) {327 t->cells[r][c].glyph = 'E';328 t->cells[r][c].attr = 0;329 }330 }331 t->dirty = true;332 break;333 default: break;334 }335 if (la >= 0) {336 t->line_attr[t->row] = (uint8_t)la;337 if (t->col > last_col(t, t->row)) t->col = last_col(t, t->row);338 t->dirty = true;339 }340 return;341 }342 if (t->intermediate == '(' || t->intermediate == ')') {343 t->g[t->intermediate == ')'] = final;344 return;345 }346 switch (final) {347 case '7': save_cursor(t); break;348 case '8': restore_cursor(t); break;349 case 'D': index_down(t); t->wrap_pending = false; t->dirty = true; break;350 case 'E': t->col = 0; index_down(t); t->wrap_pending = false; t->dirty = true; break;351 case 'M': reverse_index(t); t->wrap_pending = false; t->dirty = true; break;352 case 'H': t->tabs[t->col] = true; break;353 case 'c': full_reset(t); break;354 case '=': t->deckpam = true; break;355 case '>': t->deckpam = false; break;356 case 'Z': tx(t, "\033[?1;2c"); break;357 default: break;358 }359}360361static void control(vt100 *t, uint8_t c) {362 switch (c) {363 case 0x07: t->bells++; break;364 case 0x08:365 if (t->col > 0) t->col--;366 t->wrap_pending = false;367 t->dirty = true;368 break;369 case 0x09: {370 int lc = last_col(t, t->row);371 do t->col++;372 while (t->col < lc && !t->tabs[t->col]);373 if (t->col > lc) t->col = lc;374 t->wrap_pending = false;375 t->dirty = true;376 break;377 }378 case 0x0A:379 case 0x0B:380 case 0x0C:381 index_down(t);382 if (t->lnm) t->col = 0;383 t->wrap_pending = false;384 t->dirty = true;385 break;386 case 0x0D: t->col = 0; t->wrap_pending = false; t->dirty = true; break;387 case 0x0E: t->gl = 1; break;388 case 0x0F: t->gl = 0; break;389 default: break; /* NUL, ENQ, XON/XOFF and the rest: nothing shown */390 }391}392393void vt_receive(vt100 *t, uint8_t c) {394 c &= 0x7F;395 if (c == 0x18 || c == 0x1A) { /* CAN, SUB: abandon the sequence */396 t->state = S_GROUND;397 return;398 }399 if (c == 0x1B) {400 t->state = S_ESC;401 t->intermediate = 0;402 return;403 }404 if (c < 0x20) {405 control(t, c);406 return;407 }408 if (c == 0x7F) return;409 switch (t->state) {410 case S_GROUND: put_char(t, c); break;411 case S_ESC:412 if (c == '[') {413 t->state = S_CSI;414 t->nparams = 0;415 memset(t->params, 0, sizeof t->params);416 t->private_marker = false;417 } else if (c >= 0x20 && c <= 0x2F) {418 t->intermediate = c;419 t->state = S_ESC_INTER;420 } else {421 esc_dispatch(t, c);422 t->state = S_GROUND;423 }424 break;425 case S_ESC_INTER:426 if (c >= 0x20 && c <= 0x2F) break;427 esc_dispatch(t, c);428 t->state = S_GROUND;429 break;430 case S_CSI:431 if (c >= '0' && c <= '9') {432 if (t->nparams == 0) t->nparams = 1;433 int *p = &t->params[t->nparams - 1];434 if (*p < 10000) *p = *p * 10 + (c - '0');435 } else if (c == ';') {436 if (t->nparams == 0) t->nparams = 1;437 if (t->nparams < 16) t->params[t->nparams++] = 0;438 } else if (c == '?' && t->nparams == 0) {439 t->private_marker = true;440 } else if (c >= 0x40 && c <= 0x7E) {441 csi_dispatch(t, c);442 t->state = S_GROUND;443 } else {444 t->state = S_CSI_IGNORE;445 }446 break;447 case S_CSI_IGNORE:448 if (c >= 0x40 && c <= 0x7E) t->state = S_GROUND;449 break;450 default: t->state = S_GROUND; break;451 }452}453454void vt_receive_buf(vt100 *t, const uint8_t *buf, size_t n) {455 for (size_t i = 0; i < n; i++) vt_receive(t, buf[i]);456}457458/* ---- keyboard ---- */459460static char shifted(char c) {461 static const char from[] = "1234567890-=`[];',./\\";462 static const char to[] = "!@#$%^&*()_+~{}:\"<>?|";463 if (c >= 'a' && c <= 'z') return (char)(c - 'a' + 'A');464 const char *p = strchr(from, c);465 return p ? to[p - from] : c;466}467468/* sends the code for one press of the key; returns false for keys that send nothing */469static bool send_key(vt100 *t, int key) {470 if (key < 0x100) {471 char c = (char)key;472 if (t->shift) c = shifted(c);473 else if (t->caps && c >= 'a' && c <= 'z') c = (char)(c - 'a' + 'A');474 if (t->ctrl) {475 if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) c = (char)(c & 0x1F);476 else if (c == ' ' || c == '@') c = 0x00;477 else if (c == '[' || c == '{') c = 0x1B;478 else if (c == '\\' || c == '|') c = 0x1C;479 else if (c == ']' || c == '}') c = 0x1D;480 else if (c == '~' || c == '`') c = 0x1E;481 else if (c == '?' || c == '/') c = 0x1F;482 }483 tx_byte(t, (uint8_t)c);484 return true;485 }486 char buf[4] = {0};487 switch (key) {488 case VK_RETURN:489 case VK_KP_ENTER:490 if (key == VK_KP_ENTER && t->deckpam) tx(t, "\033OM");491 else tx(t, t->lnm ? "\r\n" : "\r");492 return true;493 case VK_LINEFEED: tx_byte(t, 0x0A); return true;494 case VK_BACKSPACE: tx_byte(t, 0x08); return true;495 case VK_DELETE: tx_byte(t, 0x7F); return true;496 case VK_TAB: tx_byte(t, 0x09); return true;497 case VK_ESC: tx_byte(t, 0x1B); return true;498 case VK_NOSCROLL:499 t->xoff_sent = !t->xoff_sent;500 tx_byte(t, t->xoff_sent ? 0x13 : 0x11);501 return true;502 case VK_UP:503 case VK_DOWN:504 case VK_RIGHT:505 case VK_LEFT:506 buf[0] = 033;507 buf[1] = t->decckm ? 'O' : '[';508 buf[2] = "ABCD"[key - VK_UP == 0 ? 0 : key == VK_DOWN ? 1 : key == VK_RIGHT ? 2 : 3];509 tx(t, buf);510 return true;511 case VK_PF1:512 case VK_PF2:513 case VK_PF3:514 case VK_PF4:515 buf[0] = 033;516 buf[1] = 'O';517 buf[2] = (char)('P' + (key - VK_PF1));518 tx(t, buf);519 return true;520 case VK_KP_MINUS:521 case VK_KP_COMMA:522 case VK_KP_PERIOD:523 if (t->deckpam) {524 buf[0] = 033;525 buf[1] = 'O';526 buf[2] = key == VK_KP_MINUS ? 'm' : key == VK_KP_COMMA ? 'l' : 'n';527 tx(t, buf);528 } else {529 tx_byte(t, key == VK_KP_MINUS ? '-' : key == VK_KP_COMMA ? ',' : '.');530 }531 return true;532 case VK_BREAK: return true; /* a break condition on the line, not a character */533 default:534 if (key >= VK_KP0 && key <= VK_KP9) {535 if (t->deckpam) {536 buf[0] = 033;537 buf[1] = 'O';538 buf[2] = (char)('p' + (key - VK_KP0));539 tx(t, buf);540 } else {541 tx_byte(t, (uint8_t)('0' + (key - VK_KP0)));542 }543 return true;544 }545 return false; /* SET-UP (not emulated) */546 }547}548549/* "a nonrepeat table contains a few keys that do not repeat. These are SET-UP, NO SCROLL, ESCAPE,550 * RETURN, BREAK, and ENTER." "A key with control cannot repeat" */551static bool repeats(const vt100 *t, int key) {552 if (t->ctrl || !t->decarm) return false;553 switch (key) {554 case VK_SETUP:555 case VK_NOSCROLL:556 case VK_ESC:557 case VK_RETURN:558 case VK_BREAK:559 case VK_KP_ENTER: return false;560 default: return true;561 }562}563564void vt_key(vt100 *t, int key, bool down, double now_ms) {565 if (key == VK_SHIFT) {566 t->shift = down;567 return;568 }569 if (key == VK_CTRL) {570 t->ctrl = down;571 return;572 }573 if (key == VK_CAPSLOCK) {574 if (down) t->caps = !t->caps;575 return;576 }577 if (!down) {578 if (key == t->held_key) t->held_key = 0;579 return;580 }581 /* "SHIFT or CTRL keys do not generate any keyclick" - every code-sending key clicks */582 if (send_key(t, key) && t->keyclick) t->clicks++;583 if (repeats(t, key)) {584 t->held_key = key;585 t->repeat_at = now_ms + REPEAT_DELAY_MS;586 } else {587 t->held_key = 0;588 }589}590591void vt_tick(vt100 *t, double now_ms) {592 int guard = 0;593 while (t->held_key && now_ms >= t->repeat_at && guard++ < 8) {594 if (send_key(t, t->held_key) && t->keyclick) t->clicks++;595 t->repeat_at += REPEAT_INTERVAL_MS;596 }597 if (t->held_key && now_ms >= t->repeat_at) t->repeat_at = now_ms + REPEAT_INTERVAL_MS;598}599600int vt_take_tx(vt100 *t, uint8_t *out, int max) {601 int n = t->tx_len < max ? t->tx_len : max;602 memcpy(out, t->tx, (size_t)n);603 memmove(t->tx, t->tx + n, (size_t)(t->tx_len - n));604 t->tx_len -= n;605 return n;606}607608/* ---- video ---- */609610/* the ROM row shown on each of the ten scans of a character row: the first scan shows row 15611 * (only line-drawing characters use it, so vertical lines join up), the rest rows 0-8 */612static int rom_row(int scan) { return scan == 0 ? 15 : scan - 1; }613614/* the 10 dots of one scan of a glyph, before the dot stretcher. "if the final bit in any row of615 * character data is set, the VT100 will 'replicate' that bit across the rest of cell" */616static unsigned glyph_dots(uint8_t glyph, int scan) {617 unsigned bits = vt100_rom[glyph & 0x7F][rom_row(scan)];618 unsigned dots = bits << 2; /* dots 0-7 in bits 9..2 */619 if (bits & 1) dots |= 3;620 return dots; /* bit 9 = leftmost dot */621}622623bool vt_render(vt100 *t, double now_ms) {624 int cursor_on = ((long)(now_ms / CURSOR_HALF_PERIOD_MS) & 1) == 0;625 int blink_on = ((long)(now_ms / BLINK_HALF_PERIOD_MS) & 1) == 0;626 int phase = cursor_on | blink_on << 1;627 t->raster_changed = false;628 if (!t->dirty && phase == t->last_phase) return false;629 t->dirty = false;630 t->last_phase = phase;631 t->raster_changed = true;632633 uint8_t on[VT_DOTS + 1];634 uint8_t cellx[VT_DOTS];635 for (int r = 0; r < VT_ROWS; r++) {636 int la = t->line_attr[r];637 int wide = la != VT_LINE_NORMAL;638 int ncols = wide ? VT_COLS / 2 : VT_COLS;639 for (int s = 0; s < VT_CELL_SCANS; s++) {640 int gs = la == VT_LINE_DOUBLE_TOP ? s / 2 : la == VT_LINE_DOUBLE_BOTTOM ? 5 + s / 2 : s;641 /* character video into the shift register */642 memset(on, 0, sizeof on);643 for (int c = 0; c < ncols; c++) {644 unsigned dots = glyph_dots(t->cells[r][c].glyph, gs);645 for (int i = 0; i < VT_CELL_DOTS; i++) {646 int bit = (dots >> (9 - i)) & 1;647 if (wide) {648 on[c * 20 + 2 * i + 1] = on[c * 20 + 2 * i + 2] = (uint8_t)bit;649 cellx[c * 20 + 2 * i] = cellx[c * 20 + 2 * i + 1] = (uint8_t)c;650 } else {651 on[c * 10 + i + 1] = (uint8_t)bit;652 cellx[c * 10 + i] = (uint8_t)c;653 }654 }655 }656 /* "The dot stretcher works by delaying the VIDEO IN H signal by one dot time ... and then657 * ORing the undelayed and delayed signals." on[x + 1] holds dot x, on[x] dot x - 1. */658 uint8_t *out = t->raster[r * VT_CELL_SCANS + s];659 for (int x = 0; x < VT_DOTS; x++) {660 int lit = on[x + 1] | on[x];661 int c = cellx[x];662 const vt_cell *cell = &t->cells[r][c];663 int attr = cell->attr;664 int is_cursor = r == t->row && c == (t->col > ncols - 1 ? ncols - 1 : t->col);665 int rev = ((attr & VT_ATTR_REVERSE) != 0) ^ t->decscnm;666 if ((attr & VT_ATTR_BLINK) && rev && !blink_on) rev ^= 1; /* blinking reverse: flips */667 if (is_cursor && t->block_cursor && cursor_on) rev ^= 1;668 int underline = gs == 8 && ((attr & VT_ATTR_UNDERLINE) || (is_cursor && !t->block_cursor && cursor_on));669 /* "Underline causes the ninth scan of a character to be forced to white of the same670 * intensity as the character for nonreversed characters, and to black for reverse" */671 int pix = underline ? !rev : (rev ? !lit : lit);672 int level;673 if (rev) {674 /* reverse characters have a dim background; bold and reverse give normal */675 level = (attr & VT_ATTR_BOLD) ? 2 : 1;676 if (is_cursor && !(attr & VT_ATTR_REVERSE) && !t->decscnm) level = 2;677 } else {678 level = (attr & VT_ATTR_BOLD) ? 3 : 2;679 /* "Blink applied to nonreverse characters causes them to alternate between their usual680 * intensity and the next lower intensity" */681 if ((attr & VT_ATTR_BLINK) && !blink_on) level--;682 }683 out[x] = pix ? (uint8_t)level : 0;684 }685 }686 }687 return true;688}689690void vt_text(const vt100 *t, char *out) {691 char *p = out;692 for (int r = 0; r < VT_ROWS; r++) {693 int ncols = t->line_attr[r] == VT_LINE_NORMAL ? VT_COLS : VT_COLS / 2;694 for (int c = 0; c < ncols; c++) {695 uint8_t g = t->cells[r][c].glyph;696 *p++ = g >= 0x20 && g < 0x7F ? (char)g : (char)(g + 0x5F); /* graphics back to their codes */697 }698 *p++ = '\n';699 }700 *p = 0;701}