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}