/* 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 "stream.h" #define MIN(a, b) ((a) < (b) ? (a) : (b)) enum { 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, }; struct Style { bool bold, italic, underline, reverse; int bg, fg; }; static const struct Style Default = { .bg = -1, .fg = -1 }; struct Cell { struct Style style; wchar_t ch; }; static uint rows, cols; static uint y, x; 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 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) { move( cell(scroll.top, 0), cell(scroll.top + 1, 0), cols * (scroll.bot - scroll.top) ); clear(cell(scroll.bot, 0), cell(scroll.bot, cols - 1)); } else { y = MIN(y + 1, rows - 1); } } break; default: { // FIXME: Nowhere for these warnings to go. if (ch < ' ') { warnx("unhandled \\x%02X", ch); return NUL; } int width = wcwidth(ch); if (width < 0) { warnx("unhandled \\u%X", ch); return NUL; } if (x + width > cols) { warnx("cannot fit '%lc'", ch); return NUL; } 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 { CSI = '[', OSC = ']', ST = '\\', }; static char updateESC(wchar_t ch) { static bool discard; if (discard) { discard = false; return NUL; } switch (ch) { case '(': discard = true; return ESC; case '=': return NUL; case '>': return NUL; case CSI: return CSI; case OSC: return OSC; // FIXME: Nowhere for this warning to go. default: warnx("unhandled ESC %lc", ch); return NUL; } } enum { Reset, Bold, Italic = 3, Underline, Reverse = 7, NotBold = 22, NotItalic, NotUnderline, NotReverse, Fg0 = 30, Fg7 = 37, Fg, FgDefault, Bg0, Bg7 = 47, Bg, BgDefault, Fg8 = 90, Fg15 = 97, Bg8 = 100, Bg15 = 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 Bold: style.bold = true; break; case Italic: style.italic = true; break; case Underline: style.underline = true; break; case Reverse: style.reverse = true; break; case NotBold: style.bold = false; break; case NotItalic: style.italic = false; break; case NotUnderline: style.underline = false; break; case NotReverse: style.reverse = false; break; case Fg: { if (++i < n && ps[i] == Color256) { if (++i < n) style.fg = ps[i]; } } break; case Bg: { if (++i < n && ps[i] == Color256) { if (++i < n) style.bg = ps[i]; } } break; case FgDefault: style.fg = -1; break; case BgDefault: style.bg = -1; break; default: { if (ps[i] >= Fg0 && ps[i] <= Fg7) { style.fg = ps[i] - Fg0; } else if (ps[i] >= Bg0 && ps[i] <= Bg7) { style.bg = ps[i] - Bg0; } else if (ps[i] >= Fg8 && ps[i] <= Fg15) { style.fg = 8 + ps[i] - Fg8; } else if (ps[i] >= Bg8 && ps[i] <= Bg15) { style.bg = 8 + ps[i] - Bg8; } } } } } // SGR toggling all attributes and setting both colors in 256 palette. enum { PSCap = 4 + 2 * 3 }; enum { DEC = '?', CUU = 'A', CUD, CUF, CUB, CNL, CPL, CHA, CUP, ED = 'J', EL, DL = 'M', DCH = 'P', VPA = 'd', SM = 'h', RM = 'l', SGR, DECSTBM = 'r', }; enum { Insert = 4, }; static char updateCSI(wchar_t ch) { static bool dec; if (ch == DEC) { dec = true; return CSI; } static uint n, p, ps[PSCap]; if (ch == ';') { n++; p++; ps[p %= PSCap] = 0; return CSI; } if (ch >= '0' && ch <= '9') { ps[p] *= 10; ps[p] += ch - '0'; if (!n) n++; return CSI; } switch (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 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)); } // FIXME: Nowhere for these warnings to go. break; case SM: { if (dec) break; switch (ps[0]) { break; case Insert: insert = true; break; default: warnx("unhandled SM %u", ps[0]); } } break; case RM: { if (dec) break; switch (ps[0]) { break; case Insert: insert = false; break; default: warnx("unhandled RM %u", ps[0]); } } break; case SGR: updateSGR(ps, p + 1); break; case DECSTBM: { scroll.top = (n > 0 ? ps[0] - 1 : 0); scroll.bot = (n > 1 ? ps[1] - 1 : rows - 1); } break; case 't': // ignore // FIXME: Nowhere for this warning to go. break; default: warnx("unhandled CSI %lc", ch); } dec = false; 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[PSCap]; uint n = 0; if (next->bold != prev->bold) { ps[n++] = (next->bold ? Bold : NotBold); } if (next->italic != prev->italic) { ps[n++] = (next->italic ? Italic : NotItalic); } if (next->underline != prev->underline) { ps[n++] = (next->underline ? Underline : NotUnderline); } if (next->reverse != prev->reverse) { ps[n++] = (next->reverse ? Reverse : NotReverse); } if (next->bg != prev->bg) { if (next->bg == -1) { ps[n++] = BgDefault; } else if (next->bg < 8) { ps[n++] = Bg0 + next->bg; } else if (next->bg < 16) { ps[n++] = Bg8 + next->bg - 8; } else { ps[n++] = Bg; ps[n++] = Color256; ps[n++] = next->bg; } } if (next->fg != prev->fg) { if (next->fg == -1) { ps[n++] = FgDefault; } else if (next->fg < 8) { ps[n++] = Fg0 + next->fg; } else if (next->fg < 16) { ps[n++] = Fg8 + next->fg - 8; } else { ps[n++] = Fg; 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%u;%u%c" "%c%c%u;%u%c", ESC, CSI, Insert, (insert ? SM : RM), ESC, CSI, scroll.top, scroll.bot, DECSTBM, ESC, CSI, y, x, CUP ); if (n < 0) goto fail; return fclose(file); fail: fclose(file); return -1; }