summaryrefslogtreecommitdiff
path: root/src/ui_terminal.cc
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui_terminal.cc')
-rw-r--r--src/ui_terminal.cc279
1 files changed, 279 insertions, 0 deletions
diff --git a/src/ui_terminal.cc b/src/ui_terminal.cc
new file mode 100644
index 0000000..f7cc023
--- /dev/null
+++ b/src/ui_terminal.cc
@@ -0,0 +1,279 @@
+#include "ui_terminal.h"
+#include <string>
+#include <cctype>
+#include <cassert>
+#include <algorithm>
+#include <set>
+#include <iostream> //TODO: Remove this
+#include <format> // Ditto
+
+namespace ui {
+
+ // Parse all commands found in https://www.man7.org/linux/man-pages/man4/console_codes.4.html
+ // Structure is inspired by https://github.com/tmux/tmux/blob/master/input.c
+ static const std::map<std::tuple<int,int,int>, commands> commandTable = {
+ // Control, intermediary, terminator -> command
+ {{0x07, 0, 0}, BEL},
+ {{0x08, 0, 0}, BS},
+ {{0x09, 0, 0}, HT},
+ {{0x0A, 0, 0}, LF},
+ {{0x0B, 0, 0}, VT},
+ {{0x0C, 0, 0}, FF},
+ {{0x0D, 0, 0}, CR},
+ {{0x0E, 0, 0}, SO},
+ {{0x0F, 0, 0}, SI},
+ {{0x7F, 0, 0}, DEL},
+ {{0x1B, '(', '0'}, SCSG0_ON},
+ {{0x1B, ')', '0'}, SCSG1_ON},
+ {{0x1B, 0, '7'}, DECSC},
+ {{0x1B, 0, '8'}, DECRC},
+ {{0x1B, '#', '8'}, DECALN},
+ {{0x1B, 0, '='}, DECPAM},
+ {{0x1B, 0, '>'}, DECPNM},
+ {{0x1B, '(', 'B'}, SCSG0_OFF},
+ {{0x1B, ')', 'B'}, SCSG1_OFF},
+ {{0x1B, 0, 'D'}, IND},
+ {{0x1B, 0, 'E'}, NEL},
+ {{0x1B, 0, 'H'}, HTS},
+ {{0x1B, 0, 'M'}, RI},
+ {{0x1B, 0, '\\'}, ST},
+ {{0x1B, 0, 'c'}, RIS},
+ // CSI sequences (ESC [ is mapped to CSI)
+ {{0x9B, 0, '@'}, ICH},
+ {{0x9B, 0, 'A'}, CUU},
+ {{0x9B, 0, 'B'}, CUD},
+ {{0x9B, 0, 'C'}, CUF},
+ {{0x9B, 0, 'D'}, CUB},
+ {{0x9B, 0, 'E'}, CNL},
+ {{0x9B, 0, 'F'}, CPL},
+ {{0x9B, 0, 'G'}, CHA},
+ {{0x9B, 0, 'H'}, CUP},
+ {{0x9B, 0, 'J'}, ED},
+ {{0x9B, 0, 'K'}, EL},
+ {{0x9B, 0, 'L'}, IL},
+ {{0x9B, 0, 'M'}, DL},
+ {{0x9B, 0, 'P'}, DCH},
+ {{0x9B, 0, 'S'}, SU},
+ {{0x9B, '?', 'S'}, SM_GRAPHICS},
+ {{0x9B, 0, 'T'}, SD},
+ {{0x9B, 0, 'X'}, ECH},
+ {{0x9B, 0, 'Z'}, CBT},
+ {{0x9B, 0, '`'}, HPA},
+ {{0x9B, 0, 'a'}, HPR},
+ {{0x9B, 0, 'b'}, REP},
+ {{0x9B, 0, 'c'}, DA},
+ {{0x9B, 0, 'd'}, VPA},
+ {{0x9B, 0, 'e'}, VPR},
+ {{0x9B, 0, 'f'}, HVP},
+ {{0x9B, 0, 'g'}, TBC},
+ {{0x9B, 0, 'h'}, SM},
+ {{0x9B, '?', 'h'}, SM_PRIVATE},
+ {{0x9B, 0, 'l'}, RM},
+ {{0x9B, '?', 'l'}, RM_PRIVATE},
+ {{0x9B, 0, 'm'}, SGR},
+ {{0x9B, 0, 'n'}, DSR},
+ {{0x9B, 0, 'r'}, DECSTBM},
+ {{0x9B, 0, 's'}, SCP},
+ {{0x9B, 0, 't'}, WINOPS},
+ {{0x9B, 0, 'u'}, RCP}
+ };
+
+ std::set<int> getIntermediaries() {
+ std::set<int> ret;
+ for(auto pair : commandTable) {
+ if(std::get<1>(pair.first)) ret.insert(std::get<1>(pair.first));
+ }
+ return ret;
+ }
+ static const std::set<int> intermediaries = getIntermediaries();
+
+ static const std::map<commands, std::string> commands2names = {
+ {BEL, "BEL"},
+ {BS, "BS"},
+ {HT, "HT"},
+ {LF, "LF"},
+ {VT, "VT"},
+ {FF, "FF"},
+ {CR, "CR"},
+ {SO, "SO"},
+ {SI, "SI"},
+ {DEL, "DEL"},
+ {DECALN, "DECALN"},
+ {DECPAM, "DECPAM"},
+ {DECPNM, "DECPNM"},
+ {DECRC, "DECRC"},
+ {DECSC, "DECSC"},
+ {HTS, "HTS"},
+ {IND, "IND"},
+ {NEL, "NEL"},
+ {RI, "RI"},
+ {RIS, "RIS"},
+ {SCSG0_OFF, "SCSG0_OFF"},
+ {SCSG0_ON, "SCSG0_ON"},
+ {SCSG1_OFF, "SCSG1_OFF"},
+ {SCSG1_ON, "SCSG1_ON"},
+ {ST, "ST"},
+ {CBT, "CBT"},
+ {CNL, "CNL"},
+ {CPL, "CPL"},
+ {CUB, "CUB"},
+ {CUD, "CUD"},
+ {CUF, "CUF"},
+ {CHA, "CHA"},
+ {CUP, "CUP"},
+ {CUU, "CUU"},
+ {DA, "DA"},
+ {DCH, "DCH"},
+ {DECSTBM, "DECSTBM"},
+ {DL, "DL"},
+ {DSR, "DSR"},
+ {ECH, "ECH"},
+ {ED, "ED"},
+ {EL, "EL"},
+ {HPA, "HPA"},
+ {HPR, "HPR"},
+ {HVP, "HVP"},
+ {ICH, "ICH"},
+ {IL, "IL"},
+ {RCP, "RCP"},
+ {REP, "REP"},
+ {RM, "RM"},
+ {RM_PRIVATE, "RM_PRIVATE"},
+ {SCP, "SCP"},
+ {SD, "SD"},
+ {SGR, "SGR"},
+ {SM, "SM"},
+ {SM_PRIVATE, "SM_PRIVATE"},
+ {SM_GRAPHICS, "SM_GRAPHICS"},
+ {SU, "SU"},
+ {TBC, "TBC"},
+ {VPA, "VPA"},
+ {VPR, "VPR"},
+ {WINOPS, "WINOPS"},
+ };
+
+ static const int NPAR=16;
+
+ struct commandParserImpl {
+ std::wstring escape;
+ std::map<commands, std::function<void(std::vector<int>)>> callbacks;
+ };
+
+
+ CommandParser::CommandParser() : data(new commandParserImpl()) {}
+
+ CommandParser::CommandParser(std::map<commands, std::function<void(std::vector<int>)>> callbacks) : data(new commandParserImpl()) {
+ data->callbacks = callbacks;
+ }
+
+ // Assumes that escape has already undergone conversion of ESC[ -> CSI, interrupting control characters processed, etc.
+ // Returns Control, intermediary, terminator, params
+ std::tuple<int,int,int,std::vector<int>> extractParts(std::wstring seq) {
+ int ctrl=0, interm=0, term=0;
+ std::vector<int> params;
+ if(seq.length() >= 1) {
+ ctrl = seq[0];
+ seq = seq.substr(1); // Peel off ctrl
+ }
+ if(seq.length() >= 1 and intermediaries.contains(seq[0])) {
+ interm = seq[0];
+ seq = seq.substr(1); // Peel off interm
+ }
+ while(seq.length() >= 1 and ((seq[0] >= '0' and seq[0] <= '9') or seq[0] == ';') and params.size() < NPAR) {
+ if(seq[0] == ';') {
+ params.push_back(0);
+ seq = seq.substr(1);
+ continue;
+ }
+ auto end = seq.find_first_not_of(L"1234567890");
+ if(end == std::string::npos) end = seq.length();
+ int param = 0;
+ try { param = (int) std::stol(seq.substr(0, end)); }
+ catch(std::exception& e) {}
+ params.push_back(param);
+ seq = seq.substr(end);
+ if(seq.length() >= 1 and seq[0] == ';') seq = seq.substr(1); // Remove trailing ';'
+ }
+ if(seq.length() >= 1) {
+ term = seq[0];
+ seq = seq.substr(1); // Peel off term
+ }
+ assert(seq.empty() && "Parsed sequence is not empty");
+ return {ctrl, interm, term, params};
+ }
+
+ std::vector<std::tuple<int,int,int>> getCmdsPossible(std::tuple<int,int,int,std::vector<int>> parsed) {
+ std::vector<std::tuple<int,int,int>> cmds;
+ for(const auto &keyval : commandTable) {
+ if((std::get<0>(parsed) == 0 or std::get<0>(parsed) == std::get<0>(keyval.first)) and
+ (std::get<1>(parsed) == 0 or std::get<1>(parsed) == std::get<1>(keyval.first)) and
+ (std::get<2>(parsed) == 0 or std::get<2>(parsed) == std::get<2>(keyval.first))) {
+ cmds.push_back(keyval.first);
+ }
+ }
+ return cmds;
+ }
+
+ std::string formatParsed(std::tuple<int,int,int,std::vector<int>> parsed, commands command) {
+ auto sanatize = [](int c){ if(c >= 32 and c < 127) return std::string(1, c); else return std::format("0x{:x}", c); };
+ std::string formatted = sanatize(std::get<0>(parsed)) + "," + sanatize(std::get<1>(parsed)) + "," + sanatize(std::get<2>(parsed));
+ if(commands2names.count(command)) formatted += "[" + commands2names.at(command) + "]";
+ formatted += "(";
+ for(int i : std::get<3>(parsed)) {
+ formatted += std::to_string(i) + ";";
+ }
+ if(formatted[formatted.length()-1] == ';') formatted = formatted.substr(0, formatted.length()-1);
+ formatted += ")";
+ return formatted;
+ }
+
+ std::tuple<commands, std::vector<int>> parseInternal(int c, std::wstring &escape) {
+ // First test if c is a control char BEL, BS, HT, LF, VT, FF, CR, SO, SI, or DEL
+ std::set<int> ctrlCharActions = {0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x7F};
+ if(ctrlCharActions.find(c) != ctrlCharActions.end()) return {commandTable.at({c,0,0}), std::vector<int>()};
+ if(c == 0x18 or c == 0x1A) { // Next handle CAN and SUB
+ escape.clear();
+ return {commands::INCOMPLETE, std::vector<int>()};
+ }
+ if(c == 0x1B or c == 0x9B) { // Finally, ESC and CSI
+ escape.clear();
+ } else if(escape.empty()) { // We are not in an escape sequence
+ return {commands::NOTCOMMAND, {c}};
+ }
+ escape.push_back(c);
+ // Handle ESC[ -> CSI conversion
+ if(escape.starts_with(L"\033[")) {
+ escape.replace(0, 2, L"\233");
+ }
+ // Handle echoed function key
+ if(escape.length() >= 3 and escape[1] == '[') {
+ escape.clear();
+ return {commands::INCOMPLETE, std::vector<int>()};
+ }
+ auto parsed = extractParts(escape);
+ auto cmds = getCmdsPossible(parsed);
+ if(cmds.empty()) {
+ escape.clear();
+ std::cerr << "\nError processing " << formatParsed(parsed, commands::INVALID) << "\n";
+ return {commands::INVALID, std::get<3>(parsed)};
+ }
+ if(cmds.size() > 1 or std::get<2>(parsed) == 0) {
+ return {commands::INCOMPLETE, std::get<3>(parsed)};
+ }
+ assert(cmds.size() == 1 && "Error parsing commands");
+ auto command = commandTable.at(cmds[0]);
+ escape.clear();
+ std::cerr << "\nSuccess processing " << formatParsed(parsed, command) << "\n";
+ return {command, std::get<3>(parsed)};
+ }
+
+ std::tuple<commands, std::vector<int>> CommandParser::parse(int c) {
+ auto parsed = parseInternal(c, this->data->escape);
+ auto command = std::get<0>(parsed);
+ if(this->data->callbacks.count(command)) {
+ if(command != commands::NOTCOMMAND) std::cerr << "Executing command " << commands2names.at(command) << "\n";
+ this->data->callbacks.at(command)(std::get<1>(parsed));
+ }
+ return {command, std::get<1>(parsed)};
+ }
+ }