/* 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 "term.h" #define MIN(a, b) ((a) < (b) ? (a) : (b)) static void unhandled(const char *format, ...) { if (isatty(STDERR_FILENO)) return; va_list ap; va_start(ap, format); char buf[256]; vsnprintf(buf, sizeof(buf), format, ap); warnx("unhandled %s", buf); va_end(ap); } static struct Cell *cell(struct Term *term, uint y, uint x) { assert(y <= term->rows); assert(x <= term->cols); assert(y * term->cols + x <= term->rows * term->cols); return &term->cells[y * term->cols + x]; } static void erase(struct Term *term, struct Cell *at, struct Cell *to) { for (; at < to; ++at) { at->style = term->style; at->ch = L' '; } } static void move(struct Cell *dst, struct Cell *src, uint len) { memmove(dst, src, sizeof(*dst) * len); } static void scrollUp(struct Term *term, uint top, uint n) { n = MIN(n, term->scroll.bot - top); move( cell(term, top, 0), cell(term, top + n, 0), term->cols * (term->scroll.bot - top - n) ); erase( term, cell(term, term->scroll.bot - n, 0), cell(term, term->scroll.bot, 0) ); } static void scrollDown(struct Term *term, uint top, uint n) { n = MIN(n, term->scroll.bot - top); move( cell(term, top + n, 0), cell(term, top, 0), term->cols * (term->scroll.bot - top - n) ); erase( term, cell(term, top, 0), cell(term, top + n, 0) ); } #define ACTION(name) \ static void name(struct Term *t, wchar_t ch __attribute__((unused))) ACTION(nop) { (void)t; } ACTION(csi) { memset(&t->param, 0, sizeof(t->param)); } ACTION(csiSep) { if (t->param.n == ParamCap) return; if (!t->param.n) t->param.n++; t->param.n++; t->param.i++; } ACTION(csiDigit) { t->param.s[t->param.i] *= 10; t->param.s[t->param.i] += ch - L'0'; if (!t->param.n) t->param.n++; } #define Y t->y #define X t->x #define B (t->rows - 1) #define R (t->cols - 1) #define C(y, x) cell(t, y, x) #define P(i, z) ((i) < t->param.n ? t->param.s[(i)] : (z)) ACTION(bs) { if (X) X--; } ACTION(ht) { X = MIN(X - X % 8 + 8, R); } ACTION(cr) { X = 0; } ACTION(cuu) { Y -= MIN(P(0, 1), Y); } ACTION(cud) { Y = MIN(Y + P(0, 1), B); } ACTION(cuf) { X = MIN(X + P(0, 1), R); } ACTION(cub) { X -= MIN(P(0, 1), X); } ACTION(cnl) { X = 0; cud(t, ch); } ACTION(cpl) { X = 0; cuu(t, ch); } ACTION(cha) { X = MIN(P(0, 1) - 1, R); } ACTION(vpa) { Y = MIN(P(0, 1) - 1, B); } ACTION(cup) { Y = MIN(P(0, 1) - 1, B); X = MIN(P(1, 1) - 1, R); } ACTION(decsc) { t->save.y = Y; t->save.x = X; } ACTION(decrc) { Y = t->save.y; X = t->save.x; } ACTION(ed) { erase( t, (P(0, 0) == 0 ? C(Y, X) : C(0, 0)), (P(0, 0) == 1 ? C(Y, X) : C(B, t->cols)) ); } ACTION(el) { erase( t, (P(0, 0) == 0 ? C(Y, X) : C(Y, 0)), (P(0, 0) == 1 ? C(Y, X) : C(Y, t->cols)) ); } ACTION(ech) { erase(t, C(Y, X), C(Y, MIN(X + P(0, 1), t->cols))); } ACTION(dch) { uint n = MIN(P(0, 1), t->cols - X); move(C(Y, X), C(Y, X + n), t->cols - X - n); erase(t, C(Y, t->cols - n), C(Y, t->cols)); } ACTION(ich) { uint n = MIN(P(0, 1), t->cols - X); move(C(Y, X + n), C(Y, X), t->cols - X - n); erase(t, C(Y, X), C(Y, X + n)); } ACTION(dl) { scrollUp(t, MIN(Y, t->scroll.bot), P(0, 1)); } ACTION(il) { scrollDown(t, MIN(Y, t->scroll.bot), P(0, 1)); } ACTION(nl) { if (Y + 1 == t->scroll.bot) { scrollUp(t, t->scroll.top, 1); } else { Y = MIN(Y + 1, B); } } ACTION(ri) { if (Y == t->scroll.top) { scrollDown(t, t->scroll.top, 1); } else { if (Y) Y--; } } ACTION(su) { scrollUp(t, t->scroll.top, P(0, 1)); } ACTION(sd) { scrollDown(t, t->scroll.top, P(0, 1)); } ACTION(decstbm) { t->scroll.bot = MIN(P(1, t->rows), t->rows); t->scroll.top = MIN(P(0, 1) - 1, t->scroll.bot); } enum { IRM = 4, DECAWM = 7, DECTCEM = 25, }; static enum Mode paramMode(struct Term *t) { enum Mode mode = 0; for (uint i = 0; i < t->param.n; ++i) { switch (t->param.s[i]) { break; case IRM: mode |= Insert; break; default: unhandled("SM/RM %u", t->param.s[i]); } } return mode; } static enum Mode paramDECMode(struct Term *t) { enum Mode mode = 0; for (uint i = 0; i < t->param.n; ++i) { switch (t->param.s[i]) { break; case 1: // DECCKM break; case DECAWM: mode |= Wrap; break; case 12: // "Start Blinking Cursor" break; case DECTCEM: mode |= Cursor; break; default: { if (t->param.s[i] < 1000) { unhandled("DECSET/DECRST %u", t->param.s[i]); } } } } return mode; } ACTION(sm) { t->mode |= paramMode(t); } ACTION(decset) { t->mode |= paramDECMode(t); } ACTION(rm) { t->mode &= ~paramMode(t); } ACTION(decrst) { t->mode &= ~paramDECMode(t); } enum { Reset, SetBold, SetDim, SetItalic, SetUnderline, SetBlink, SetReverse = 7, UnsetBoldDim = 22, UnsetItalic, UnsetUnderline, UnsetBlink, UnsetReverse = 27, SetFg0 = 30, SetFg7 = 37, SetFg, ResetFg, SetBg0 = 40, SetBg7 = 47, SetBg, ResetBg, SetFg8 = 90, SetFgF = 97, SetBg8 = 100, SetBgF = 107, Color256 = 5, }; static const struct Style Default = { .bg = -1, .fg = -1 }; ACTION(sgr) { uint n = t->param.i + 1; for (uint i = 0; i < n; ++i) { switch (t->param.s[i]) { break; case Reset: t->style = Default; break; case SetBold: t->style.attr &= ~Dim; t->style.attr |= Bold; break; case SetDim: t->style.attr &= ~Bold; t->style.attr |= Dim; break; case SetItalic: t->style.attr |= Italic; break; case SetUnderline: t->style.attr |= Underline; break; case SetBlink: t->style.attr |= Blink; break; case SetReverse: t->style.attr |= Reverse; break; case UnsetBoldDim: t->style.attr &= ~(Bold | Dim); break; case UnsetItalic: t->style.attr &= ~Italic; break; case UnsetUnderline: t->style.attr &= ~Underline; break; case UnsetBlink: t->style.attr &= ~Blink; break; case UnsetReverse: t->style.attr &= ~Reverse; break; case SetFg: { if (++i < n && t->param.s[i] == Color256) { if (++i < n) t->style.fg = t->param.s[i]; } } break; case SetBg: { if (++i < n && t->param.s[i] == Color256) { if (++i < n) t->style.bg = t->param.s[i]; } } break; case ResetFg: t->style.fg = Default.fg; break; case ResetBg: t->style.bg = Default.bg; break; default: { if (t->param.s[i] >= SetFg0 && t->param.s[i] <= SetFg7) { t->style.fg = t->param.s[i] - SetFg0; } else if (t->param.s[i] >= SetBg0 && t->param.s[i] <= SetBg7) { t->style.bg = t->param.s[i] - SetBg0; } else if (t->param.s[i] >= SetFg8 && t->param.s[i] <= SetFgF) { t->style.fg = 8 + t->param.s[i] - SetFg8; } else if (t->param.s[i] >= SetBg8 && t->param.s[i] <= SetBgF) { t->style.bg = 8 + t->param.s[i] - SetBg8; } } } } } enum { USASCII, DECSpecial, }; ACTION(usascii) { t->charset = USASCII; } ACTION(decSpecial) { t->charset = DECSpecial; } static const wchar_t AltCharset[128] = { ['`'] = L'◆', ['a'] = L'▒', ['f'] = L'°', ['g'] = L'±', ['i'] = L'␋', ['j'] = L'┘', ['k'] = L'┐', ['l'] = L'┌', ['m'] = L'└', ['n'] = L'┼', ['o'] = L'⎺', ['p'] = L'⎻', ['q'] = L'─', ['r'] = L'⎼', ['s'] = L'⎽', ['t'] = L'├', ['u'] = L'┤', ['v'] = L'┴', ['w'] = L'┬', ['x'] = L'│', ['y'] = L'≤', ['z'] = L'≥', ['{'] = L'π', ['|'] = L'≠', ['}'] = L'£', ['~'] = L'·', }; ACTION(add) { if (t->charset == DECSpecial && ch < 128 && AltCharset[ch]) { ch = AltCharset[ch]; } int width = wcwidth(ch); if (width < 0) { unhandled("\\u%02X", ch); return; } if (t->mode & Insert) { uint n = MIN((uint)width, t->cols - X); move(C(Y, X + n), C(Y, X), t->cols - X - n); } if (t->mode & Wrap && X + width > t->cols) { cr(t, ch); nl(t, ch); } C(Y, X)->style = t->style; C(Y, X)->ch = ch; for (int i = 1; i < width && X + i < t->cols; ++i) { C(Y, X + i)->style = t->style; C(Y, X + i)->ch = L'\0'; } X = MIN(X + width, (t->mode & Wrap ? t->cols : R)); } enum { Data, Esc, G0, CSI, CSILt, CSIEq, CSIGt, CSIQm, CSIInter, OSC, OSCEsc, }; ACTION(escDefault) { (void)t; unhandled("ESC %lc", ch); } ACTION(g0Default) { unhandled("G0 %lc", ch); t->charset = USASCII; } ACTION(csiInter) { switch (t->state) { break; case CSI: unhandled("CSI %lc ...", ch); break; case CSILt: unhandled("CSI < %lc ...", ch); break; case CSIEq: unhandled("CSI = %lc ...", ch); break; case CSIGt: unhandled("CSI > %lc ...", ch); break; case CSIQm: unhandled("CSI ? %lc ...", ch); } } ACTION(csiFinal) { switch (t->state) { break; case CSI: unhandled("CSI %lc", ch); break; case CSILt: unhandled("CSI < %lc", ch); break; case CSIEq: unhandled("CSI = %lc", ch); break; case CSIGt: unhandled("CSI > %lc", ch); break; case CSIQm: unhandled("CSI ? %lc", ch); break; case CSIInter: unhandled("CSI ... %lc", ch); } } #define S(s) break; case s: switch (ch) #define A(c, a, s) break; case c: a(term, ch); term->state = s #define D(a, s) break; default: a(term, ch); term->state = s void termUpdate(struct Term *term, wchar_t ch) { switch (term->state) { default: abort(); S(Data) { A(BEL, nop, Data); A(BS, bs, Data); A(HT, ht, Data); A(NL, nl, Data); A(CR, cr, Data); A(ESC, nop, Esc); D(add, Data); } S(Esc) { A('(', nop, G0); A('7', decsc, Data); A('8', decrc, Data); A('=', nop, Data); A('>', nop, Data); A('M', ri, Data); A('[', csi, CSI); A(']', nop, OSC); D(escDefault, Data); } S(G0) { A('0', decSpecial, Data); A('B', usascii, Data); D(g0Default, Data); } S(CSI) { A(' ' ... '/', csiInter, CSIInter); A('0' ... '9', csiDigit, CSI); A(':', nop, CSI); A(';', csiSep, CSI); A('<', nop, CSILt); A('=', nop, CSIEq); A('>', nop, CSIGt); A('?', nop, CSIQm); A('@', ich, Data); A('A', cuu, Data); A('B', cud, Data); A('C', cuf, Data); A('D', cub, Data); A('E', cnl, Data); A('F', cpl, Data); A('G', cha, Data); A('H', cup, Data); A('J', ed, Data); A('K', el, Data); A('L', il, Data); A('M', dl, Data); A('P', dch, Data); A('S', su, Data); A('T', sd, Data); A('X', ech, Data); A('d', vpa, Data); A('h', sm, Data); A('l', rm, Data); A('m', sgr, Data); A('r', decstbm, Data); A('t', nop, Data); D(csiFinal, Data); } S(CSILt ... CSIGt) { A(' ' ... '/', csiInter, CSIInter); A('0' ... '9', csiDigit, term->state); A(':', nop, term->state); A(';', csiSep, term->state); D(csiFinal, Data); } S(CSIQm) { A(' ' ... '/', csiInter, CSIInter); A('0' ... '9', csiDigit, CSIQm); A(':', nop, CSIQm); A(';', csiSep, CSIQm); A('h', decset, Data); A('l', decrst, Data); D(csiFinal, Data); } S(CSIInter) { D(csiFinal, Data); } S(OSC) { A(BEL, nop, Data); A(ESC, nop, OSCEsc); D(nop, OSC); } S(OSCEsc) { A('\\', nop, Data); D(nop, OSC); } } } struct Term *termAlloc(uint rows, uint cols) { size_t size = sizeof(struct Term) + sizeof(struct Cell) * rows * cols; struct Term *term = malloc(size); if (!term) err(EX_OSERR, "malloc"); memset(term, 0, sizeof(*term)); term->rows = rows; term->cols = cols; term->mode = Wrap | Cursor; term->scroll.bot = rows; term->style = Default; erase(term, cell(term, 0, 0), cell(term, rows - 1, cols - 1)); return term; } 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, "\33[m"); uint p[ParamCap]; uint n = 0; enum Attr diff = prev->attr ^ next->attr; if (diff & (Bold | Dim)) { p[n++] = ( next->attr & Bold ? SetBold : next->attr & Dim ? SetDim : UnsetBoldDim ); } if (diff & Italic) { p[n++] = (next->attr & Italic ? SetItalic : UnsetItalic); } if (diff & Underline) { p[n++] = (next->attr & Underline ? SetUnderline : UnsetUnderline); } if (diff & Blink) { p[n++] = (next->attr & Blink ? SetBlink : UnsetBlink); } if (diff & Reverse) { p[n++] = (next->attr & Reverse ? SetReverse : UnsetReverse); } if (next->bg != prev->bg) { if (next->bg == -1) { p[n++] = ResetBg; } else if (next->bg < 8) { p[n++] = SetBg0 + next->bg; } else if (next->bg < 16) { p[n++] = SetBg8 + next->bg - 8; } else { p[n++] = SetBg; p[n++] = Color256; p[n++] = next->bg; } } if (next->fg != prev->fg) { if (next->fg == -1) { p[n++] = ResetFg; } else if (next->fg < 8) { p[n++] = SetFg0 + next->fg; } else if (next->fg < 16) { p[n++] = SetFg8 + next->fg - 8; } else { p[n++] = SetFg; p[n++] = Color256; p[n++] = next->fg; } } if (0 > fprintf(file, "\33[%u", p[0])) return -1; for (uint i = 1; i < n; ++i) { if (0 > fprintf(file, ";%u", p[i])) return -1; } return fprintf(file, "m"); } int termSnapshot(struct Term *term, int _fd) { int fd = dup(_fd); if (fd < 0) return -1; FILE *file = fdopen(fd, "w"); if (!file) return -1; int n = fprintf(file, "\33[%dl\33[?%dl\33[r\33[H\33[m", IRM, DECAWM); if (n < 0) goto fail; struct Style prev = Default; for (uint y = 0; y < term->rows; ++y) { if (y && 0 > fprintf(file, "\r\n")) goto fail; for (uint x = 0; x < term->cols; ++x) { struct Cell *c = cell(term, y, x); if (!c->ch) continue; if (0 > styleDiff(file, &prev, &c->style)) goto fail; if (0 > fprintf(file, "%lc", c->ch)) goto fail; prev = c->style; } } if (0 > styleDiff(file, &prev, &term->style)) goto fail; n = fprintf( file, "\33[%d%c" "\33[?%d%c" "\33[?%d%c" "\33[%u;%ur" "\33[%u;%uH", IRM, (term->mode & Insert ? 'h' : 'l'), DECAWM, (term->mode & Wrap ? 'h' : 'l'), DECTCEM, (term->mode & Cursor ? 'h' : 'l'), 1 + term->scroll.top, term->scroll.bot, 1 + term->y, 1 + term->x ); if (n < 0) goto fail; return fclose(file); fail: fclose(file); return -1; } #ifdef TERM_MAIN #include int main(void) { setlocale(LC_CTYPE, ""); struct Term *term = termAlloc(24, 80); wint_t ch; while (WEOF != (ch = getwchar())) { termUpdate(term, ch); } } #endif