/* Copyright (C) 2019 C. McEnroe * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include #include "stream.h" enum C0 { NUL, SOH, STX, ETX, EOT, ENQ, ACK, BEL, BS, HT, NL, VT, NP, CR, SO, SI, DLE, DC1, DC2, DC3, DC4, NAK, SYN, ETB, CAN, EM, SUB, ESC, FS, GS, RS, US, DEL = 0x7F, }; static char unhandled(const char *format, ...) { if (isatty(STDERR_FILENO)) return NUL; va_list ap; va_start(ap, format); char buf[256]; vsnprintf(buf, sizeof(buf), format, ap); warnx("unhandled %s", buf); va_end(ap); return NUL; } static const struct Style Default = { .bg = -1, .fg = -1 }; static uint rows, cols; static uint y, x; static bool cursor = true; static bool insert; static struct { uint top, bot; } scroll; static struct Style style; static struct Cell *cells; static struct Cell *cell(uint y, uint x) { return &cells[y * cols + x]; } static void clear(struct Cell *a, struct Cell *b) { for (; a <= b; ++a) { a->style = style; a->ch = ' '; } } static void move(struct Cell *dst, struct Cell *src, uint len) { memmove(dst, src, sizeof(*dst) * len); } static void scrollUp(uint n) { move( cell(scroll.top, 0), cell(scroll.top + n, 0), cols * (scroll.bot - scroll.top - (n - 1)) ); clear(cell(scroll.bot - (n - 1), 0), cell(scroll.bot, cols - 1)); } static void scrollDown(uint n) { move( cell(scroll.top + n, 0), cell(scroll.top, 0), cols * (scroll.bot - scroll.top - (n - 1)) ); clear(cell(scroll.top, 0), cell(scroll.top + (n - 1), cols - 1)); } static char updateNUL(wchar_t ch) { switch (ch) { break; case ESC: return ESC; break; case BS: if (x) x--; break; case CR: x = 0; break; case NL: { if (y == scroll.bot) { scrollUp(1); } else { y = MIN(y + 1, rows - 1); } } break; default: { int width = wcwidth(ch); if (ch < ' ') return unhandled("\\x%02X", ch); if (width < 0) return unhandled("\\u%X", ch); if (x + width > cols) return unhandled("'%lc' too wide", ch); if (insert) { move(cell(y, x + width), cell(y, x), cols - x - width); } cell(y, x)->style = style; cell(y, x)->ch = ch; for (int i = 1; i < width; ++i) { cell(y, x + i)->style = style; cell(y, x + i)->ch = '\0'; } x = MIN(x + width, cols - 1); } } return NUL; } enum C1 { RI = 'M', CSI = '[', ST = '\\', OSC = ']', }; static char updateESC(wchar_t ch) { static bool discard; if (discard) { discard = false; return NUL; } switch (ch) { break; case CSI: return CSI; break; case OSC: return OSC; break; case RI: scrollDown(1); break; case '(': discard = true; return ESC; break; case '=': // ignore break; case '>': // ignore break; default: return unhandled("ESC %lc", ch); } return NUL; } enum SGR { Reset, SetBold, SetDim, SetItalic, SetUnderline, SetBlink, SetReverse = 7, UnsetBoldDim = 22, UnsetItalic, UnsetUnderline, UnsetBlink, UnsetReverse = 27, SetFg0 = 30, SetFg7 = 37, SetFg, ResetFg, SetBg0, SetBg7 = 47, SetBg, ResetBg, SetFg8 = 90, SetFg15 = 97, SetBg8 = 100, SetBg15 = 107, Color256 = 5, }; static void updateSGR(uint ps[], uint n) { for (uint i = 0; i < n; ++i) { switch (ps[i]) { break; case Reset: style = Default; break; case SetBold: style.attr &= ~Dim; style.attr |= Bold; break; case SetDim: style.attr &= ~Bold; style.attr |= Dim; break; case SetItalic: style.attr |= Italic; break; case SetUnderline: style.attr |= Underline; break; case SetBlink: style.attr |= Blink; break; case SetReverse: style.attr |= Reverse; break; case UnsetBoldDim: style.attr &= ~(Bold | Dim); break; case UnsetItalic: style.attr &= ~Italic; break; case UnsetUnderline: style.attr &= ~Underline; break; case UnsetBlink: style.attr &= ~Blink; break; case UnsetReverse: style.attr &= ~Reverse; break; case SetFg: { if (++i < n && ps[i] == Color256) { if (++i < n) style.fg = ps[i]; } } break; case SetBg: { if (++i < n && ps[i] == Color256) { if (++i < n) style.bg = ps[i]; } } break; case ResetFg: style.fg = -1; break; case ResetBg: style.bg = -1; break; default: { if (ps[i] >= SetFg0 && ps[i] <= SetFg7) { style.fg = ps[i] - SetFg0; } else if (ps[i] >= SetBg0 && ps[i] <= SetBg7) { style.bg = ps[i] - SetBg0; } else if (ps[i] >= SetFg8 && ps[i] <= SetFg15) { style.fg = 8 + ps[i] - SetFg8; } else if (ps[i] >= SetBg8 && ps[i] <= SetBg15) { style.bg = 8 + ps[i] - SetBg8; } } } } } // SGR toggling all attributes and setting both colors in 256 palette. enum { CSIMax = 5 + 2 * 3 }; enum CSI { DEC = '?', CUU = 'A', CUD, CUF, CUB, CNL, CPL, CHA, CUP, ED = 'J', EL, DL = 'M', DCH = 'P', SU = 'S', SD = 'T', ECH = 'X', VPA = 'd', SM = 'h', RM = 'l', SGR, DECSTBM = 'r', DECSET = DEC + SM, DECRST = DEC + RM, }; enum Mode { IRM = 4, DECCKM = 1, DECAWM = 7, DECTCEM = 25, }; static char updateCSI(wchar_t ch) { static char dec; if (ch == DEC) { dec = DEC; return CSI; } static uint n, p, ps[CSIMax]; if (ch == ';') { n++; p++; ps[p %= CSIMax] = 0; return CSI; } if (ch >= '0' && ch <= '9') { ps[p] *= 10; ps[p] += ch - '0'; if (!n) n++; return CSI; } switch (dec + ch) { break; case CUU: y -= MIN((n ? ps[0] : 1), y); break; case CUD: y = MIN(y + (n ? ps[0] : 1), rows - 1); break; case CUF: x = MIN(x + (n ? ps[0] : 1), cols - 1); break; case CUB: x -= MIN((n ? ps[0] : 1), x); break; case CNL: y = MIN(y + (n ? ps[0] : 1), rows - 1); x = 0; break; case CPL: y -= MIN((n ? ps[0] : 1), y); x = 0; break; case CHA: x = MIN((n ? ps[0] - 1 : 0), cols - 1); break; case VPA: y = MIN((n ? ps[0] - 1 : 0), rows - 1); break; case CUP: { y = MIN((n > 0 ? ps[0] - 1 : 0), rows - 1); x = MIN((n > 1 ? ps[1] - 1 : 0), cols - 1); } break; case ED: { struct Cell *a = cell(0, 0); struct Cell *b = cell(rows - 1, cols - 1); if (ps[0] == 0) a = cell(y, x); if (ps[0] == 1) b = cell(y, x); clear(a, b); } break; case EL: { struct Cell *a = cell(y, 0); struct Cell *b = cell(y, cols - 1); if (ps[0] == 0) a = cell(y, x); if (ps[0] == 1) b = cell(y, x); clear(a, b); } break; case ECH: { struct Cell *a = cell(y, x); struct Cell *b = cell(y, MIN(x + (n > 0 ? ps[0] - 1 : 0), cols - 1)); clear(a, b); } break; case DL: { uint i = MIN((n ? ps[0] : 1), rows - y); move(cell(y, 0), cell(y + i, 0), cols * (rows - y - i)); clear(cell(rows - i, 0), cell(rows - 1, cols - 1)); } break; case DCH: { uint i = MIN((n ? ps[0] : 1), cols - x); move(cell(y, x), cell(y, x + i), cols - x - i); clear(cell(y, cols - i), cell(y, cols - 1)); } break; case SU: scrollUp(MIN((n ? ps[0] : 1), scroll.bot - scroll.top)); break; case SD: scrollDown(MIN((n ? ps[0] : 1), scroll.bot - scroll.top)); break; case SM: { switch (ps[0]) { break; case IRM: insert = true; break; default: unhandled("SM %u", ps[0]); } } break; case RM: { switch (ps[0]) { break; case IRM: insert = false; break; default: unhandled("RM %u", ps[0]); } } break; case SGR: updateSGR(ps, p + 1); break; case DECSTBM: { // FIXME: Prevent bot < top. scroll.top = (n > 0 ? ps[0] - 1 : 0); scroll.bot = (n > 1 ? ps[1] - 1 : rows - 1); } break; case DECSET: { switch (ps[0]) { break; case DECCKM: // ignore break; case DECAWM: // TODO break; case 12: // ignore break; case DECTCEM: cursor = true; break; default: if (ps[0] < 1000) unhandled("DECSET %u", ps[0]); } } break; case DECRST: { switch (ps[0]) { break; case DECCKM: // ignore break; case DECAWM: // TODO break; case 12: // ignore break; case DECTCEM: cursor = false; break; default: if (ps[0] < 1000) unhandled("DECRST %u", ps[0]); } } break; case 't': // ignore break; default: unhandled("CSI %s%lc", (dec ? "? " : ""), ch); } dec = 0; ps[n = p = 0] = 0; return NUL; } static char updateOSC(wchar_t ch) { static bool esc; switch (ch) { break; case BEL: return NUL; break; case ESC: esc = true; break; case ST: { if (!esc) break; esc = false; return NUL; } } return OSC; } void termUpdate(wchar_t ch) { static char seq; switch (seq) { break; case NUL: seq = updateNUL(ch); break; case ESC: seq = updateESC(ch); break; case CSI: seq = updateCSI(ch); break; case OSC: seq = updateOSC(ch); } } void termInit(uint _rows, uint _cols) { rows = _rows; cols = _cols; cells = calloc(rows * cols, sizeof(*cells)); if (!cells) err(EX_OSERR, "calloc"); style = Default; clear(cell(0, 0), cell(rows - 1, cols - 1)); scroll.bot = rows - 1; } static int styleDiff(FILE *file, const struct Style *prev, const struct Style *next) { if (!memcmp(prev, next, sizeof(*prev))) return 0; if (!memcmp(next, &Default, sizeof(*next))) { return fprintf(file, "%c%c%c", ESC, CSI, SGR); } uint ps[CSIMax]; uint n = 0; enum Attr diff = prev->attr ^ next->attr; if (diff & (Bold | Dim)) { ps[n++] = ( next->attr & Bold ? SetBold : next->attr & Dim ? SetDim : UnsetBoldDim ); } if (diff & Italic) { ps[n++] = (next->attr & Italic ? SetItalic : UnsetItalic); } if (diff & Underline) { ps[n++] = (next->attr & Underline ? SetUnderline : UnsetUnderline); } if (diff & Blink) { ps[n++] = (next->attr & Blink ? SetBlink : UnsetBlink); } if (diff & Reverse) { ps[n++] = (next->attr & Reverse ? SetReverse : UnsetReverse); } if (next->bg != prev->bg) { if (next->bg == -1) { ps[n++] = ResetBg; } else if (next->bg < 8) { ps[n++] = SetBg0 + next->bg; } else if (next->bg < 16) { ps[n++] = SetBg8 + next->bg - 8; } else { ps[n++] = SetBg; ps[n++] = Color256; ps[n++] = next->bg; } } if (next->fg != prev->fg) { if (next->fg == -1) { ps[n++] = ResetFg; } else if (next->fg < 8) { ps[n++] = SetFg0 + next->fg; } else if (next->fg < 16) { ps[n++] = SetFg8 + next->fg - 8; } else { ps[n++] = SetFg; ps[n++] = Color256; ps[n++] = next->fg; } } if (0 > fprintf(file, "%c%c%u", ESC, CSI, ps[0])) return -1; for (uint i = 1; i < n; ++i) { if (0 > fprintf(file, ";%u", ps[i])) return -1; } return fprintf(file, "%c", SGR); } int termSnapshot(int _fd) { int fd = dup(_fd); if (fd < 0) return -1; FILE *file = fdopen(fd, "w"); if (!file) return -1; struct Style prev = Default; for (uint y = 0; y < rows; ++y) { if (y && 0 > fprintf(file, "\r\n")) goto fail; for (uint x = 0; x < cols; ++x) { if (!cell(y, x)->ch) continue; if (0 > styleDiff(file, &prev, &cell(y, x)->style)) goto fail; if (0 > fprintf(file, "%lc", cell(y, x)->ch)) goto fail; prev = cell(y, x)->style; } } if (0 > styleDiff(file, &prev, &style)) goto fail; int n = fprintf( file, "%c%c%d%c" "%c%c%c%d%c" "%c%c%u;%u%c" "%c%c%u;%u%c", ESC, CSI, IRM, (insert ? SM : RM), ESC, CSI, DEC, DECTCEM, (cursor ? DECSET : DECRST) - DEC, ESC, CSI, 1 + scroll.top, 1 + scroll.bot, DECSTBM, ESC, CSI, 1 + y, 1 + x, CUP ); if (n < 0) goto fail; return fclose(file); fail: fclose(file); return -1; } struct Display termDisplay(void) { return (struct Display) { .cursor = cursor, .rows = rows, .cols = cols, .y = y, .x = x, .cells = cells, }; }