/* 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" 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 *t, uint y, uint x) { assert(y < t->rows); assert(x < t->cols); return &t->cells[y * t->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 *t, uint n) { move( cell(t, t->scroll.top, 0), cell(t, t->scroll.top + n, 0), t->cols * (1 + t->scroll.bot - t->scroll.top - n) ); erase( t->style, cell(t, 1 + t->scroll.bot - n, 0), cell(t, t->scroll.bot, t->cols - 1) ); } static void scrollDown(struct Term *t, uint n) { move( cell(t, t->scroll.top + n, 0), cell(t, t->scroll.top, 0), t->cols * (1 + t->scroll.bot - t->scroll.top - n) ); erase( t->style, cell(t, t->scroll.top, 0), cell(t, t->scroll.top + n - 1, t->cols - 1) ); } typedef void Action(struct Term *, wchar_t ch); #define ACTION(name) \ static void name(struct Term *t, wchar_t _ch __attribute__((__unused__))) enum { G0 = '(', CSI = '[', ST = '\\', OSC = ']', }; ACTION(nop) { (void)t; } ACTION(esc) { t->state = ESC; } ACTION(g0) { t->state = G0; } ACTION(osc) { t->state = OSC; } ACTION(st) { t->state = ST; } 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(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(dl) { uint n = MIN(P(0, 1), t->rows - Y); move(C(Y, 0), C(Y + n, 0), t->cols * (t->rows - Y - n)); erase(t->style, C(t->rows - n, 0), C(B, R)); } 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->style, C(Y, t->cols - n), C(Y, R)); } ACTION(il) { uint n = MIN(P(0, 1), t->rows - Y); move(C(Y + n, 0), C(Y, 0), t->cols * (t->rows - Y - n)); erase(t->style, C(Y, 0), C(Y + n - 1, R)); } 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->style, C(Y, X), C(Y, X + n - 1)); } ACTION(nl) { if (Y == t->scroll.bot) { scrollUp(t, 1); } else { Y = MIN(Y + 1, B); } } ACTION(ind) { scrollUp(t, 1); } ACTION(ri) { scrollDown(t, 1); } ACTION(su) { scrollUp(t, MIN(P(0, 1), t->scroll.bot - t->scroll.top)); } ACTION(sd) { scrollDown(t, MIN(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: // ignore break; case DECAWM: mode |= Wrap; break; case DECTCEM: mode |= Cursor; break; default: 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); } } static Action *Actions[][128] = { [NUL][0] = add, [NUL][BEL] = nop, [NUL][BS] = bs, [NUL][NL] = nl, [NUL][CR] = cr, [NUL][ESC] = esc, [ESC][0] = escUnhandled, [ESC]['('] = g0, [ESC]['7'] = decsc, [ESC]['8'] = decrc, [ESC]['='] = nop, [ESC]['>'] = nop, [ESC]['D'] = ind, [ESC]['M'] = ri, [ESC]['['] = csi, [ESC][']'] = osc, [G0][0] = nop, [CSI][0] = csiParam, [CSI]['@'] = ich, [CSI]['A'] = cuu, [CSI]['B'] = cud, [CSI]['C'] = cuf, [CSI]['D'] = cub, [CSI]['E'] = cnl, [CSI]['F'] = cpl, [CSI]['G'] = cha, [CSI]['H'] = cup, [CSI]['J'] = ed, [CSI]['K'] = el, [CSI]['L'] = il, [CSI]['M'] = dl, [CSI]['P'] = dch, [CSI]['S'] = su, [CSI]['T'] = sd, [CSI]['X'] = ech, [CSI]['d'] = vpa, [CSI]['h'] = mode, [CSI]['l'] = mode, [CSI]['m'] = sgr, [CSI]['r'] = decstbm, [CSI]['t'] = nop, [OSC][0] = osc, [OSC][BEL] = nop, [OSC][ESC] = st, [ST][0] = osc, [ST]['\\'] = nop, }; void termUpdate(struct Term *term, wchar_t ch) { assert((uint)term->state < sizeof(Actions) / sizeof(Actions[0])); Action *action = NULL; if (ch < 128) action = Actions[(uint)term->state][ch]; if (!action) action = Actions[(uint)term->state][0]; assert(action); term->state = NUL; action(term, ch); } 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.top = 0; 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; }