#include "terminal.h" #include namespace roro::term { namespace { // A character the screen's font has, for one it may not. uint8_t glyphFor(uint32_t cp) { if (cp >= 0x20 && cp <= 0x7E) return static_cast(cp); if (cp >= 0xA0 && cp <= 0xFF) return static_cast(cp); if (cp >= 0x2500 && cp <= 0x257F) { // box drawing switch (cp) { case 0x2500: case 0x2501: case 0x2504: case 0x2505: case 0x2508: case 0x2509: case 0x254C: case 0x254D: case 0x2550: return '-'; case 0x2502: case 0x2503: case 0x2506: case 0x2507: case 0x250A: case 0x250B: case 0x254E: case 0x254F: case 0x2551: return '|'; default: return '+'; } } switch (cp) { case 0x2018: case 0x2019: return '\''; case 0x201C: case 0x201D: return '"'; case 0x2010: case 0x2011: case 0x2012: case 0x2013: case 0x2014: return '-'; case 0x2022: case 0x25CF: return '*'; case 0x2026: return '.'; case 0x2190: return '<'; case 0x2192: return '>'; case 0x2191: return '^'; case 0x2193: return 'v'; case 0x2588: case 0x2593: case 0x2592: case 0x2591: return '#'; default: return '?'; } } // The DEC "special graphics" set: lines drawn with the letters j to x. uint8_t lineGlyph(uint8_t c) { switch (c) { case 'q': return '-'; case 'x': return '|'; case 'j': case 'k': case 'l': case 'm': case 'n': case 't': case 'u': case 'v': case 'w': return '+'; case '`': return '*'; case 'a': return '#'; case '~': return '*'; default: return c; } } // One of 256 colours, or a colour given as red, green and blue, as the nearest of the sixteen. int nearest16(int r, int g, int b) { int most = std::max(r, std::max(g, b)); if (most < 48) return 0; int half = most / 2; int colour = (r > half ? 1 : 0) | (g > half ? 2 : 0) | (b > half ? 4 : 0); if (colour == 7 && most < 200) return most < 110 ? 8 : 7; return most > 170 ? colour | 8 : colour; } int from256(int n) { if (n < 16) return n; if (n >= 232) return nearest16(8 + (n - 232) * 10, 8 + (n - 232) * 10, 8 + (n - 232) * 10); n -= 16; static const int kSteps[6] = {0, 95, 135, 175, 215, 255}; return nearest16(kSteps[n / 36], kSteps[(n / 6) % 6], kSteps[n % 6]); } } // namespace void colourRgb(int index, uint8_t& r, uint8_t& g, uint8_t& b) { static const uint8_t kTable[16][3] = {{0, 0, 0}, {205, 49, 49}, {13, 188, 121}, {229, 229, 16}, {36, 114, 200}, {188, 63, 188}, {17, 168, 205}, {204, 204, 204}, {102, 102, 102}, {241, 76, 76}, {35, 209, 139}, {245, 245, 67}, {59, 142, 234}, {214, 112, 214}, {41, 184, 219}, {255, 255, 255}}; r = kTable[index & 15][0]; g = kTable[index & 15][1]; b = kTable[index & 15][2]; } Terminal::Terminal(int cols, int rows, int historyLines) : cols_(std::max(2, cols)), rows_(std::max(2, rows)), historyMax_(std::max(0, historyLines)), mainGrid_(static_cast(cols_ * rows_)), altGrid_(static_cast(cols_ * rows_)), bottom_(rows_ - 1) {} Cell Terminal::blank() const { Cell c; c.attr = static_cast((bg_ << 4) | 7); return c; } bool Terminal::takeBell() { bool b = bell_; bell_ = false; return b; } std::string Terminal::rowText(int row) const { std::string s; for (int c = 0; c < cols_; c++) s += static_cast(cell(row, c).ch); size_t end = s.find_last_not_of(' '); s.resize(end == std::string::npos ? 0 : end + 1); return s; } int Terminal::param(size_t i, int fallback) const { return i < params_.size() && params_[i] > 0 ? params_[i] : fallback; } void Terminal::moveTo(int row, int col) { row_ = std::clamp(row, 0, rows_ - 1); col_ = std::clamp(col, 0, cols_ - 1); wrapPending_ = false; } void Terminal::eraseCells(int row, int from, int to) { Cell b = blank(); for (int c = std::max(0, from); c <= std::min(cols_ - 1, to); c++) grid()[static_cast(row * cols_ + c)] = b; } void Terminal::scrollUp(int top, int bottom, int n, bool toHistory) { n = std::min(n, bottom - top + 1); auto& g = grid(); for (int i = 0; i < n; i++) { if (toHistory && !alt_ && top == 0 && historyMax_ > 0) { // off the top of the real screen: kept, as text history_.push_back(rowText(0)); if (static_cast(history_.size()) > historyMax_) history_.pop_front(); } std::move(g.begin() + (top + 1) * cols_, g.begin() + (bottom + 1) * cols_, g.begin() + top * cols_); eraseCells(bottom, 0, cols_ - 1); } } void Terminal::scrollDown(int top, int bottom, int n) { n = std::min(n, bottom - top + 1); auto& g = grid(); for (int i = 0; i < n; i++) { std::move_backward(g.begin() + top * cols_, g.begin() + bottom * cols_, g.begin() + (bottom + 1) * cols_); eraseCells(top, 0, cols_ - 1); } } void Terminal::lineFeed() { wrapPending_ = false; if (row_ == bottom_) scrollUp(top_, bottom_, 1); else if (row_ < rows_ - 1) row_++; } void Terminal::reverseIndex() { wrapPending_ = false; if (row_ == top_) scrollDown(top_, bottom_, 1); else if (row_ > 0) row_--; } void Terminal::put(uint32_t cp) { uint8_t ch = lineDrawing_ && cp < 0x80 ? lineGlyph(static_cast(cp)) : glyphFor(cp); if (wrapPending_) { col_ = 0; lineFeed(); } uint8_t fg = static_cast(bold_ && fg_ < 8 ? fg_ | 8 : fg_), bg = bg_; if (inverse_) std::swap(fg, bg); Cell& c = grid()[static_cast(row_ * cols_ + col_)]; c.ch = ch; c.attr = static_cast((bg << 4) | (fg & 15)); if (col_ == cols_ - 1) wrapPending_ = autoWrap_; else col_++; } void Terminal::control(uint8_t c) { switch (c) { case 0x07: bell_ = true; break; case 0x08: if (col_ > 0) col_--; wrapPending_ = false; break; case 0x09: moveTo(row_, std::min(cols_ - 1, (col_ / 8 + 1) * 8)); break; case 0x0A: case 0x0B: case 0x0C: lineFeed(); break; case 0x0D: col_ = 0; wrapPending_ = false; break; default: break; } } void Terminal::reset() { alt_ = false; Cell b; std::fill(mainGrid_.begin(), mainGrid_.end(), b); std::fill(altGrid_.begin(), altGrid_.end(), b); row_ = col_ = top_ = 0; bottom_ = rows_ - 1; fg_ = 7; bg_ = 0; bold_ = inverse_ = wrapPending_ = appCursor_ = lineDrawing_ = false; autoWrap_ = cursorShown_ = true; } void Terminal::escape(uint8_t c) { state_ = State::Ground; switch (c) { case '[': state_ = State::Csi; params_.clear(); paramStarted_ = private_ = false; break; case ']': case 'P': case '^': case '_': state_ = State::Osc; break; // a title, or something this doesn't read: skipped to its end case '(': case ')': case '*': case '+': state_ = State::Charset; break; case '7': savedRow_ = row_; savedCol_ = col_; savedAttr_ = static_cast((bg_ << 4) | fg_); break; case '8': moveTo(savedRow_, savedCol_); fg_ = savedAttr_ & 15; bg_ = savedAttr_ >> 4; break; case 'D': lineFeed(); break; case 'E': col_ = 0; lineFeed(); break; case 'M': reverseIndex(); break; case 'c': reset(); break; default: break; // = and >, the keypad's modes, among others } } void Terminal::sgr() { if (params_.empty()) params_.push_back(0); for (size_t i = 0; i < params_.size(); i++) { int p = params_[i]; if (p == 0) { fg_ = 7; bg_ = 0; bold_ = inverse_ = false; } else if (p == 1) bold_ = true; else if (p == 7) inverse_ = true; else if (p == 22) bold_ = false; else if (p == 27) inverse_ = false; else if (p >= 30 && p <= 37) fg_ = static_cast(p - 30); else if (p == 39) fg_ = 7; else if (p >= 40 && p <= 47) bg_ = static_cast(p - 40); else if (p == 49) bg_ = 0; else if (p >= 90 && p <= 97) fg_ = static_cast(p - 90 + 8); else if (p >= 100 && p <= 107) bg_ = static_cast(p - 100 + 8); else if ((p == 38 || p == 48) && i + 1 < params_.size()) { int colour = -1; if (params_[i + 1] == 5 && i + 2 < params_.size()) { colour = from256(params_[i + 2] & 255); i += 2; } else if (params_[i + 1] == 2 && i + 4 < params_.size()) { colour = nearest16(params_[i + 2] & 255, params_[i + 3] & 255, params_[i + 4] & 255); i += 4; } else { break; } (p == 38 ? fg_ : bg_) = static_cast(colour); } } } void Terminal::mode(bool on) { for (int p : params_) { if (!private_) continue; switch (p) { case 1: appCursor_ = on; break; case 7: autoWrap_ = on; break; case 25: cursorShown_ = on; break; case 47: case 1047: case 1049: if (on == alt_) break; if (on && p == 1049) { savedRow_ = row_; savedCol_ = col_; } alt_ = on; if (on) std::fill(altGrid_.begin(), altGrid_.end(), Cell()); top_ = 0; bottom_ = rows_ - 1; if (!on && p == 1049) moveTo(savedRow_, savedCol_); else if (on) moveTo(0, 0); break; default: break; // the mouse, bracketed paste and the rest } } } void Terminal::csi(uint8_t final) { int n = param(0, 1); switch (final) { case 'A': moveTo(std::max(row_ - n, row_ >= top_ ? top_ : 0), col_); break; case 'B': case 'e': moveTo(std::min(row_ + n, row_ <= bottom_ ? bottom_ : rows_ - 1), col_); break; case 'C': case 'a': moveTo(row_, col_ + n); break; case 'D': moveTo(row_, col_ - n); break; case 'E': moveTo(row_ + n, 0); break; case 'F': moveTo(row_ - n, 0); break; case 'G': case '`': moveTo(row_, n - 1); break; case 'd': moveTo(n - 1, col_); break; case 'H': case 'f': moveTo(param(0, 1) - 1, param(1, 1) - 1); break; case 'J': { int what = params_.empty() ? 0 : params_[0]; if (what == 0) { eraseCells(row_, col_, cols_ - 1); for (int r = row_ + 1; r < rows_; r++) eraseCells(r, 0, cols_ - 1); } else if (what == 1) { for (int r = 0; r < row_; r++) eraseCells(r, 0, cols_ - 1); eraseCells(row_, 0, col_); } else { for (int r = 0; r < rows_; r++) eraseCells(r, 0, cols_ - 1); } break; } case 'K': { int what = params_.empty() ? 0 : params_[0]; eraseCells(row_, what == 0 ? col_ : 0, what == 1 ? col_ : cols_ - 1); break; } case 'L': if (row_ >= top_ && row_ <= bottom_) scrollDown(row_, bottom_, n); break; case 'M': if (row_ >= top_ && row_ <= bottom_) scrollUp(row_, bottom_, n, false); // deleted, not scrolled away: not history break; case 'P': { n = std::min(n, cols_ - col_); auto row = grid().begin() + row_ * cols_; std::move(row + col_ + n, row + cols_, row + col_); eraseCells(row_, cols_ - n, cols_ - 1); break; } case '@': { n = std::min(n, cols_ - col_); auto row = grid().begin() + row_ * cols_; std::move_backward(row + col_, row + cols_ - n, row + cols_); eraseCells(row_, col_, col_ + n - 1); break; } case 'X': eraseCells(row_, col_, col_ + n - 1); break; case 'S': scrollUp(top_, bottom_, n); break; case 'T': scrollDown(top_, bottom_, n); break; case 'm': sgr(); break; case 'r': { int top = param(0, 1) - 1, bottom = param(1, rows_) - 1; if (top < bottom && bottom < rows_) { top_ = top; bottom_ = bottom; } else { top_ = 0; bottom_ = rows_ - 1; } moveTo(0, 0); break; } case 's': savedRow_ = row_; savedCol_ = col_; break; case 'u': moveTo(savedRow_, savedCol_); break; case 'h': mode(true); break; case 'l': mode(false); break; case 'n': if (!reply) break; if (param(0, 0) == 6) reply("\x1b[" + std::to_string(row_ + 1) + ";" + std::to_string(col_ + 1) + "R"); else if (param(0, 0) == 5) reply("\x1b[0n"); break; case 'c': if (reply && !private_) reply("\x1b[?6c"); // "a VT102" break; default: break; } } void Terminal::feed(const uint8_t* data, size_t len) { for (size_t i = 0; i < len; i++) { uint8_t c = data[i]; if (state_ == State::Osc || state_ == State::OscEsc) { // skipped: to a bell, or to ESC backslash if (c == 0x07 || (state_ == State::OscEsc && c == '\\')) state_ = State::Ground; else state_ = c == 0x1B ? State::OscEsc : State::Osc; continue; } if (c == 0x1B) { state_ = State::Esc; utf8Left_ = 0; continue; } if (c < 0x20) { // these act wherever they come, in the middle of a sequence too if (c == 0x0E) lineDrawing_ = true; else if (c == 0x0F) lineDrawing_ = false; else control(c); continue; } switch (state_) { case State::Esc: escape(c); break; case State::Charset: lineDrawing_ = c == '0'; state_ = State::Ground; break; case State::Csi: if (c >= '0' && c <= '9') { if (!paramStarted_) { if (params_.size() < 16) params_.push_back(0); paramStarted_ = true; } if (!params_.empty() && params_.back() < 10000) params_.back() = params_.back() * 10 + (c - '0'); } else if (c == ';' || c == ':') { if (!paramStarted_ && params_.size() < 16) params_.push_back(0); paramStarted_ = false; } else if (c == '?' || c == '>' || c == '=' || c == '<') { private_ = true; } else if (c >= 0x40 && c <= 0x7E) { state_ = State::Ground; csi(c); } // anything else is an in-between byte: passed over break; default: if (c == 0x7F) break; if (c < 0x80) { utf8Left_ = 0; put(c); } else if (c >= 0xC0) { // the first byte of a longer character utf8Left_ = c >= 0xF0 ? 3 : c >= 0xE0 ? 2 : 1; utf8_ = c & (c >= 0xF0 ? 0x07 : c >= 0xE0 ? 0x0F : 0x1F); } else if (utf8Left_ > 0) { utf8_ = (utf8_ << 6) | (c & 0x3F); if (--utf8Left_ == 0) put(utf8_); } else { put('?'); // a byte that belongs to nothing } break; } } revision_++; } void Terminal::resize(int cols, int rows) { cols = std::max(2, cols); rows = std::max(2, rows); if (cols == cols_ && rows == rows_) return; // The cursor's line stays on screen: what is above it goes to the history if it has to. int shift = alt_ ? 0 : std::max(0, row_ - (rows - 1)); if (shift) { int oldTop = top_, oldBottom = bottom_; top_ = 0; bottom_ = rows_ - 1; scrollUp(0, rows_ - 1, shift); top_ = oldTop; bottom_ = oldBottom; } auto copy = [&](std::vector& g) { std::vector fresh(static_cast(cols * rows)); for (int r = 0; r < std::min(rows, rows_); r++) for (int c = 0; c < std::min(cols, cols_); c++) fresh[static_cast(r * cols + c)] = g[static_cast(r * cols_ + c)]; g.swap(fresh); }; copy(mainGrid_); copy(altGrid_); cols_ = cols; rows_ = rows; top_ = 0; bottom_ = rows_ - 1; moveTo(row_ - shift, col_); savedRow_ = std::min(savedRow_, rows_ - 1); savedCol_ = std::min(savedCol_, cols_ - 1); revision_++; } std::string encodeKey(TermKey key, uint32_t ch, bool ctrl, bool alt, bool appCursorKeys) { std::string out; auto arrow = [&](char letter) { return std::string(appCursorKeys ? "\x1bO" : "\x1b[") + letter; }; switch (key) { case TermKey::Up: out = arrow('A'); break; case TermKey::Down: out = arrow('B'); break; case TermKey::Right: out = arrow('C'); break; case TermKey::Left: out = arrow('D'); break; case TermKey::Enter: out = "\r"; break; case TermKey::Backspace: out = "\x7f"; break; case TermKey::Tab: out = "\t"; break; case TermKey::Escape: out = "\x1b"; break; case TermKey::PageUp: out = "\x1b[5~"; break; case TermKey::PageDown: out = "\x1b[6~"; break; case TermKey::Char: if (ctrl) { uint32_t c = ch >= 'a' && ch <= 'z' ? ch - 32 : ch; if (c >= '@' && c <= '_') out = std::string(1, static_cast(c - '@')); else if (c == ' ' || c == '2') out = std::string(1, '\0'); else if (c == '?' || c == '8') out = "\x7f"; else if (c == '3') out = "\x1b"; else if (c == '4') out = "\x1c"; else if (c == '5') out = "\x1d"; else if (c == '6') out = "\x1e"; else if (c == '7' || c == '/') out = "\x1f"; break; } if (ch < 0x80) out = std::string(1, static_cast(ch)); else if (ch < 0x800) out = {static_cast(0xC0 | (ch >> 6)), static_cast(0x80 | (ch & 0x3F))}; else if (ch < 0x10000) out = {static_cast(0xE0 | (ch >> 12)), static_cast(0x80 | ((ch >> 6) & 0x3F)), static_cast(0x80 | (ch & 0x3F))}; else out = {static_cast(0xF0 | (ch >> 18)), static_cast(0x80 | ((ch >> 12) & 0x3F)), static_cast(0x80 | ((ch >> 6) & 0x3F)), static_cast(0x80 | (ch & 0x3F))}; break; } if (alt && !out.empty() && key != TermKey::Escape) out.insert(0, 1, '\x1b'); return out; } } // namespace roro::term