Public Access
SSH client: a terminal on another machine (#2)
The SSH App opens one session to a shell, over libssh (LibSSH-ESP32 5.10.0) on mbedTLS. A server is trusted the first time on its fingerprint, and a changed key is a warning with Cancel selected. The password is typed each time and kept nowhere; or the device makes itself an Ed25519 key, whose public half is shown, written to /ssh/id_ed25519.pub and printed by `ssh status`. lib/term is the terminal: what a shell, less, top, nano and vim send, with sixteen colours, scroll regions, the alternate screen and 100 lines of scrollback. Five text sizes with Ctrl and + or -, from 60x20 to 26x8, told to the far end. The session goes on when the App is left; SSH shows in the Status Bar. `ssh user@host` in the Shell opens the App. Also: - Keys that aren't characters carry Shift, Ctrl and Alt. The terminal needs it, and it makes Ctrl+Fn+up/down in a note and Shift+Tab in Gemini work from the real keyboard. - IRC doesn't try to connect under 60 KB free: started with a session open, its TLS handshake took the heap down to 236 bytes. - libssh's own curve25519 is left out of the build (scripts/libssh_filter.py): libsodium's has the same names. Costs 292 KB of flash and about 50 KB of heap while a session is open; not started under 75 KB free. Docs: guide page, how-to, FAQ, home page, Status Bar, SD card folders, the memory how-to, README, glossary, N1 notes with Q254 to Q266 and the checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
#include "ssh_hosts.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace roro::term {
|
||||
|
||||
namespace {
|
||||
std::vector<std::string> linesOf(const std::string& text) {
|
||||
std::vector<std::string> out;
|
||||
for (size_t at = 0; at < text.size();) {
|
||||
size_t end = text.find('\n', at);
|
||||
if (end == std::string::npos) end = text.size();
|
||||
if (end > at) out.push_back(text.substr(at, end - at));
|
||||
at = end + 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
bool hostChars(const std::string& s) {
|
||||
if (s.empty() || s.size() > 253) return false;
|
||||
for (char c : s)
|
||||
if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '.' || c == '-')) return false;
|
||||
return s.front() != '.' && s.front() != '-';
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::string SshTarget::text() const { return user + "@" + host + (port == 22 ? "" : ":" + std::to_string(port)); }
|
||||
std::string SshTarget::hostPort() const { return host + ":" + std::to_string(port); }
|
||||
|
||||
std::string parseSshTarget(const std::string& text, SshTarget& out) {
|
||||
size_t at = text.find('@');
|
||||
if (at == std::string::npos || at == 0) return "user@host, please";
|
||||
SshTarget t;
|
||||
t.user = text.substr(0, at);
|
||||
std::string rest = text.substr(at + 1);
|
||||
if (t.user.size() > 32 || t.user.find_first_of(" @:/") != std::string::npos) return "that isn't a user name";
|
||||
size_t colon = rest.rfind(':');
|
||||
if (colon != std::string::npos) {
|
||||
std::string port = rest.substr(colon + 1);
|
||||
long n = 0;
|
||||
if (port.empty() || port.size() > 5) return "a port from 1 to 65535";
|
||||
for (char c : port) {
|
||||
if (c < '0' || c > '9') return "a port from 1 to 65535";
|
||||
n = n * 10 + (c - '0');
|
||||
}
|
||||
if (n < 1 || n > 65535) return "a port from 1 to 65535";
|
||||
t.port = static_cast<uint16_t>(n);
|
||||
rest.resize(colon);
|
||||
}
|
||||
if (!hostChars(rest)) return "that isn't a host";
|
||||
t.host = rest;
|
||||
out = t;
|
||||
return "";
|
||||
}
|
||||
|
||||
SshHosts::SshHosts(const std::string& stored) {
|
||||
for (auto& line : linesOf(stored)) {
|
||||
SshTarget t;
|
||||
if (hosts_.size() < kMax && parseSshTarget(line, t).empty()) hosts_.push_back(t.text());
|
||||
}
|
||||
}
|
||||
|
||||
void SshHosts::used(const SshTarget& target) {
|
||||
std::string text = target.text();
|
||||
hosts_.erase(std::remove(hosts_.begin(), hosts_.end(), text), hosts_.end());
|
||||
hosts_.insert(hosts_.begin(), text);
|
||||
if (hosts_.size() > kMax) hosts_.resize(kMax);
|
||||
}
|
||||
|
||||
void SshHosts::remove(size_t index) {
|
||||
if (index < hosts_.size()) hosts_.erase(hosts_.begin() + static_cast<long>(index));
|
||||
}
|
||||
|
||||
std::string SshHosts::stored() const {
|
||||
std::string s;
|
||||
for (auto& h : hosts_) s += h + "\n";
|
||||
return s;
|
||||
}
|
||||
|
||||
SshKnownHosts::SshKnownHosts(const std::string& stored) {
|
||||
for (auto& line : linesOf(stored)) {
|
||||
size_t space = line.find(' ');
|
||||
if (space != std::string::npos && space > 0 && space + 1 < line.size() && known_.size() < kMax) known_.emplace_back(line.substr(0, space), line.substr(space + 1));
|
||||
}
|
||||
}
|
||||
|
||||
std::string SshKnownHosts::fingerprintOf(const std::string& hostPort) const {
|
||||
for (auto& k : known_)
|
||||
if (k.first == hostPort) return k.second;
|
||||
return "";
|
||||
}
|
||||
|
||||
void SshKnownHosts::remember(const std::string& hostPort, const std::string& fingerprint) {
|
||||
known_.erase(std::remove_if(known_.begin(), known_.end(), [&](const std::pair<std::string, std::string>& k) { return k.first == hostPort; }), known_.end());
|
||||
known_.emplace_back(hostPort, fingerprint);
|
||||
if (known_.size() > kMax) known_.erase(known_.begin());
|
||||
}
|
||||
|
||||
void SshKnownHosts::forget(const std::string& hostPort) {
|
||||
known_.erase(std::remove_if(known_.begin(), known_.end(), [&](const std::pair<std::string, std::string>& k) { return k.first == hostPort; }), known_.end());
|
||||
}
|
||||
|
||||
std::string SshKnownHosts::stored() const {
|
||||
std::string s;
|
||||
for (auto& k : known_) s += k.first + " " + k.second + "\n";
|
||||
return s;
|
||||
}
|
||||
|
||||
} // namespace roro::term
|
||||
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Who the SSH App connects to, and which servers it has met (issue #2, N1 Q262 and Q263): kept
|
||||
// in the device's settings as a few lines of text.
|
||||
namespace roro::term {
|
||||
|
||||
struct SshTarget {
|
||||
std::string user, host;
|
||||
uint16_t port = 22;
|
||||
std::string text() const; // user@host, with :port when it isn't 22
|
||||
std::string hostPort() const; // host:port, always: what a host key is remembered under
|
||||
};
|
||||
// "user@host", "user@host:2222". "" or what is wrong with it.
|
||||
std::string parseSshTarget(const std::string& text, SshTarget& out);
|
||||
|
||||
// Up to eight, the last used first; one a line.
|
||||
class SshHosts {
|
||||
public:
|
||||
static constexpr size_t kMax = 8;
|
||||
explicit SshHosts(const std::string& stored = "");
|
||||
const std::vector<std::string>& list() const { return hosts_; }
|
||||
void used(const SshTarget& target); // to the front, added if it's new
|
||||
void remove(size_t index);
|
||||
std::string stored() const;
|
||||
|
||||
private:
|
||||
std::vector<std::string> hosts_;
|
||||
};
|
||||
|
||||
// The fingerprint each server showed the first time: "host:port SHA256:...", one a line, sixteen
|
||||
// at most (the oldest goes).
|
||||
class SshKnownHosts {
|
||||
public:
|
||||
static constexpr size_t kMax = 16;
|
||||
explicit SshKnownHosts(const std::string& stored = "");
|
||||
std::string fingerprintOf(const std::string& hostPort) const; // "" if never met
|
||||
void remember(const std::string& hostPort, const std::string& fingerprint);
|
||||
void forget(const std::string& hostPort);
|
||||
std::string stored() const;
|
||||
|
||||
private:
|
||||
std::vector<std::pair<std::string, std::string>> known_;
|
||||
};
|
||||
|
||||
} // namespace roro::term
|
||||
@@ -0,0 +1,495 @@
|
||||
#include "terminal.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
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<uint8_t>(cp);
|
||||
if (cp >= 0xA0 && cp <= 0xFF) return static_cast<uint8_t>(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<size_t>(cols_ * rows_)),
|
||||
altGrid_(static_cast<size_t>(cols_ * rows_)), bottom_(rows_ - 1) {}
|
||||
|
||||
Cell Terminal::blank() const {
|
||||
Cell c;
|
||||
c.attr = static_cast<uint8_t>((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<char>(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<size_t>(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<int>(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<uint8_t>(cp)) : glyphFor(cp);
|
||||
if (wrapPending_) {
|
||||
col_ = 0;
|
||||
lineFeed();
|
||||
}
|
||||
uint8_t fg = static_cast<uint8_t>(bold_ && fg_ < 8 ? fg_ | 8 : fg_), bg = bg_;
|
||||
if (inverse_) std::swap(fg, bg);
|
||||
Cell& c = grid()[static_cast<size_t>(row_ * cols_ + col_)];
|
||||
c.ch = ch;
|
||||
c.attr = static_cast<uint8_t>((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<uint8_t>((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<uint8_t>(p - 30);
|
||||
else if (p == 39) fg_ = 7;
|
||||
else if (p >= 40 && p <= 47) bg_ = static_cast<uint8_t>(p - 40);
|
||||
else if (p == 49) bg_ = 0;
|
||||
else if (p >= 90 && p <= 97) fg_ = static_cast<uint8_t>(p - 90 + 8);
|
||||
else if (p >= 100 && p <= 107) bg_ = static_cast<uint8_t>(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<uint8_t>(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<Cell>& g) {
|
||||
std::vector<Cell> fresh(static_cast<size_t>(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<size_t>(r * cols + c)] = g[static_cast<size_t>(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<char>(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<char>(ch));
|
||||
else if (ch < 0x800) out = {static_cast<char>(0xC0 | (ch >> 6)), static_cast<char>(0x80 | (ch & 0x3F))};
|
||||
else if (ch < 0x10000) out = {static_cast<char>(0xE0 | (ch >> 12)), static_cast<char>(0x80 | ((ch >> 6) & 0x3F)), static_cast<char>(0x80 | (ch & 0x3F))};
|
||||
else out = {static_cast<char>(0xF0 | (ch >> 18)), static_cast<char>(0x80 | ((ch >> 12) & 0x3F)), static_cast<char>(0x80 | ((ch >> 6) & 0x3F)),
|
||||
static_cast<char>(0x80 | (ch & 0x3F))};
|
||||
break;
|
||||
}
|
||||
if (alt && !out.empty() && key != TermKey::Escape) out.insert(0, 1, '\x1b');
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace roro::term
|
||||
@@ -0,0 +1,96 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// A terminal's screen (issue #2, N1 Q256): what a remote program's output makes of a grid of
|
||||
// characters. It understands what a shell, `less`, `top`, `nano` and plain `vim` send: the cursor,
|
||||
// erasing, sixteen colours, scroll regions, the alternate screen. No mouse. Characters are kept
|
||||
// as the screen's font has them (Latin-1); what it lacks becomes `?`, and box-drawing lines
|
||||
// become + - |.
|
||||
namespace roro::term {
|
||||
|
||||
struct Cell {
|
||||
uint8_t ch = ' ';
|
||||
uint8_t attr = 0x07; // low four bits: the colour of the character, high four: of what is behind it
|
||||
};
|
||||
|
||||
class Terminal {
|
||||
public:
|
||||
Terminal(int cols, int rows, int historyLines);
|
||||
|
||||
void feed(const uint8_t* data, size_t len);
|
||||
// A new size: what is on screen stays where it is, from the top left; if the cursor would fall
|
||||
// off the bottom, the top lines go to the history.
|
||||
void resize(int cols, int rows);
|
||||
// What the program asked to be told (where the cursor is, what kind of terminal this is).
|
||||
std::function<void(const std::string&)> reply;
|
||||
|
||||
int cols() const { return cols_; }
|
||||
int rows() const { return rows_; }
|
||||
const Cell& cell(int row, int col) const { return grid()[static_cast<size_t>(row * cols_ + col)]; }
|
||||
int cursorRow() const { return row_; }
|
||||
int cursorCol() const { return col_; }
|
||||
bool cursorVisible() const { return cursorShown_; }
|
||||
bool appCursorKeys() const { return appCursor_; } // the arrows are sent another way (vim, less)
|
||||
bool altScreen() const { return alt_; }
|
||||
uint32_t revision() const { return revision_; } // changes whenever the screen may have
|
||||
bool takeBell();
|
||||
|
||||
// Lines that scrolled off the top of the main screen, oldest first; their text only.
|
||||
int historyCount() const { return static_cast<int>(history_.size()); }
|
||||
const std::string& historyLine(int i) const { return history_[static_cast<size_t>(i)]; }
|
||||
|
||||
std::string rowText(int row) const; // without trailing spaces
|
||||
|
||||
private:
|
||||
enum class State { Ground, Esc, Csi, Osc, OscEsc, Charset };
|
||||
|
||||
std::vector<Cell>& grid() { return alt_ ? altGrid_ : mainGrid_; }
|
||||
const std::vector<Cell>& grid() const { return alt_ ? altGrid_ : mainGrid_; }
|
||||
Cell blank() const;
|
||||
void put(uint32_t codePoint);
|
||||
void control(uint8_t c);
|
||||
void escape(uint8_t c);
|
||||
void csi(uint8_t final);
|
||||
void sgr();
|
||||
void mode(bool on);
|
||||
void lineFeed();
|
||||
void reverseIndex();
|
||||
void scrollUp(int top, int bottom, int n, bool toHistory = true);
|
||||
void scrollDown(int top, int bottom, int n);
|
||||
void eraseCells(int row, int from, int to);
|
||||
void moveTo(int row, int col);
|
||||
void reset();
|
||||
int param(size_t i, int fallback) const;
|
||||
|
||||
int cols_, rows_, historyMax_;
|
||||
std::vector<Cell> mainGrid_, altGrid_;
|
||||
std::deque<std::string> history_;
|
||||
bool alt_ = false;
|
||||
int row_ = 0, col_ = 0, top_ = 0, bottom_ = 0;
|
||||
int savedRow_ = 0, savedCol_ = 0;
|
||||
uint8_t savedAttr_ = 0x07;
|
||||
bool wrapPending_ = false, autoWrap_ = true, cursorShown_ = true, appCursor_ = false, lineDrawing_ = false, bell_ = false;
|
||||
uint8_t fg_ = 7, bg_ = 0;
|
||||
bool bold_ = false, inverse_ = false;
|
||||
State state_ = State::Ground;
|
||||
std::vector<int> params_;
|
||||
bool paramStarted_ = false, private_ = false;
|
||||
uint32_t utf8_ = 0;
|
||||
int utf8Left_ = 0;
|
||||
uint32_t revision_ = 0;
|
||||
};
|
||||
|
||||
// What a key sends to the remote program.
|
||||
enum class TermKey { Char, Up, Down, Left, Right, Enter, Backspace, Tab, Escape, PageUp, PageDown };
|
||||
std::string encodeKey(TermKey key, uint32_t ch, bool ctrl, bool alt, bool appCursorKeys);
|
||||
|
||||
// One of the terminal's sixteen colours as red, green and blue.
|
||||
void colourRgb(int index, uint8_t& r, uint8_t& g, uint8_t& b);
|
||||
|
||||
} // namespace roro::term
|
||||
Reference in New Issue
Block a user