/* 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)) #define MAX(a, b) ((a) > (b) ? (a) : (b)) #define BOX(l, x, u) MIN(MAX((x), (l)), (u)) 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); return &term->cells[y * term->cols + x]; } static void erase(struct Style style, struct Cell *a, struct Cell *b) { for (; a <= b; ++a) { a->style = style; a->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) { move( cell(term, top, 0), cell(term, top + n, 0), term->cols * (1 + term->scroll.bot - top - n) ); erase( term->style, cell(term, 1 + term->scroll.bot - n, 0), cell(term, term->scroll.bot, term->cols - 1) ); } static void scrollDown(struct Term *term, uint top, uint n) { move( cell(term, top + n, 0), cell(term, top, 0), term->cols * (1 + term->scroll.bot - top - n) ); erase( term->style, cell(term, top, 0), cell(term, top + n - 1, term->cols - 1) ); } typedef void Action(struct Term *, wchar_t); #define ACTION(name) \ static void name(struct Term *t, wchar_t _ch __attribute__((unused))) enum { Def, Esc, G0, CSI, OSC, OSCEsc, }; ACTION(nop) { (void)t; } ACTION(esc) { t->state = Esc; } ACTION(g0) { t->state = G0; } ACTION(osc) { t->state = OSC; } ACTION(oscEsc) { t->state = OSCEsc; } ACTION(csi) { t->state = CSI; memset(&t->param, 0, sizeof(t->param)); } static void csiParam(struct Term *t, wchar_t ch) { if (ch == L'?') { t->param.q = true; } else if (ch == L';') { t->param.n++; t->param.i++; t->param.i %= ParamCap; } else if (ch >= L'0' && ch <= L'9') { t->param.s[t->param.i] *= 10; t->param.s[t->param.i] += ch - L'0'; if (!t->param.n) t->param.n++; } else { // FIXME: Allow other characters in CSI params. unhandled("CSI %lc", ch); return; } t->state = CSI; } static void escUnhandled(struct Term *t, wchar_t ch) { (void)t; unhandled("ESC %lc", ch); } #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, 0); } ACTION(cpl) { X = 0; cuu(t, 0); } 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->style, (P(0, 0) == 0 ? C(Y, X) : C(0, 0)), (P(0, 0) == 1 ? C(Y, X) : C(B, R)) ); } ACTION(el) { erase( t->style, (P(0, 0) == 0 ? C(Y, X) : C(Y, 0)), (P(0, 0) == 1 ? C(Y, X) : C(Y, R)) ); } ACTION(ech) { erase(t->style, C(Y, X), C(Y, MIN(X + P(0, 1) - 1, R))); } ACTION(dch) { uint n = BOX(1, P(0, 1), R - X); move(C(Y, X), C(Y, X + n), t->cols - X - n); erase(t->style, C(Y, t->cols - n), C(Y, R)); } ACTION(ich) { uint n = BOX(1, P(0, 1), R - X); move(C(Y, X + n), C(Y, X), t->cols - X - n); erase(t->style, C(Y, X), C(Y, X + n - 1)); } ACTION(dl) { scrollUp(t, Y, BOX(1, P(0, 1), t->scroll.bot - Y)); } ACTION(il) { scrollDown(t, Y, BOX(1, P(0, 1), t->scroll.bot - Y)); } ACTION(nl) { if (Y == 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, BOX(1, P(0, 1), t->scroll.bot - t->scroll.top)); } ACTION(sd) { scrollDown(t, t->scroll.top, BOX(1, P(0, 1), t->scroll.bot - t->scroll.top)); } ACTION(decstbm) { t->scroll.bot = MIN(P(1, t->rows) - 1, B); t->scroll.top = MIN(P(0, 1) - 1, t->scroll.bot); } enum { IRM = 4, DECAWM = 7, DECTCEM = 25, }; static void mode(struct Term *t, wchar_t ch) { enum Mode mode = 0; for (uint i = 0; i < t->param.n; ++i) { if (t->param.q) { 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]); } } } } else { switch (t->param.s[i]) { break; case IRM: mode |= Insert; break; default: unhandled("SM/RM %u", t->param.s[i]); } } } t->mode = (ch == L'h' ? t->mode | mode : t->mode & ~mode); } 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; } } } } } static void add(struct Term *t, wchar_t ch) { int width = wcwidth(ch); if (width < 0) { unhandled("\\u%02X", ch); return; } if (X + width > t->cols) { unhandled("'%lc' too wide", ch); return; } if (t->mode & Insert) { move(C(Y, X + width), C(Y, X), t->cols - X - width); } C(Y, X)->style = t->style; C(Y, X)->ch = ch; for (int i = 1; i < width; ++i) { C(Y, X + i)->style = t->style; C(Y, X + i)->ch = L'\0'; } if (t->mode & Wrap && X + width >= t->cols) { cr(t, 0); nl(t, 0); } else { X = MIN(X + width, R); } } #define S(s) break; case (s): switch (ch) #define A(c, a) break; case (c): (a)(term, ch) #define D(a) break; default: (a)(term, ch) void termUpdate(struct Term *term, wchar_t ch) { uint state = term->state; term->state = Def; switch (state) { default: abort(); S(Def) { A(BEL, nop); A(BS, bs); A(HT, ht); A(NL, nl); A(CR, cr); A(ESC, esc); D(add); } S(Esc) { A('(', g0); A('7', decsc); A('8', decrc); A('=', nop); A('>', nop); A('M', ri); A('[', csi); A(']', osc); D(escUnhandled); } S(G0) { D(nop); } S(CSI) { A('@', ich); A('A', cuu); A('B', cud); A('C', cuf); A('D', cub); A('E', cnl); A('F', cpl); A('G', cha); A('H', cup); A('J', ed); A('K', el); A('L', il); A('M', dl); A('P', dch); A('S', su); A('T', sd); A('X', ech); A('d', vpa); A('h', mode); A('l', mode); A('m', sgr); A('r', decstbm); A('t', nop); D(csiParam); } S(OSC) { A(BEL, nop); A(ESC, oscEsc); D(osc); } S(OSCEsc) { A('\\', nop); D(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 - 1; term->style = Default; erase(Default, 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, 1 + 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