SSH client: a terminal on another machine (#2)
CI / build (pull_request) Successful in 2m22s
Site / build (pull_request) Successful in 10s

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:
2026-10-08 04:38:36 +02:00
co-authored by Claude Opus 5.5
parent 7223147f26
commit 6b71aace4f
47 changed files with 2801 additions and 20 deletions
+96
View File
@@ -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