door.cpp 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458
  1. #include "door.h"
  2. #include <algorithm>
  3. #include <chrono>
  4. #include <ctype.h>
  5. #include <string.h>
  6. #include <string>
  7. #include <thread>
  8. #include <libgen.h> // basename
  9. // time/date output std::put_time()
  10. // https://en.cppreference.com/w/cpp/io/manip/put_time
  11. #include <ctime>
  12. #include <iomanip>
  13. // alarm signal
  14. #include <signal.h>
  15. #include <unistd.h> //
  16. #include <iconv.h>
  17. #include <algorithm>
  18. #include <iostream>
  19. #include <pwd.h> // getpwuid
  20. /**
  21. * @file
  22. * @brief Door
  23. */
  24. /*
  25. My strategy here has failed.
  26. If I set enigma to cp437, then it handles everything but the cp437
  27. symbols (diamonds/hearts/spades/clubs) correctly on the unicode side.
  28. [And my door thinks it's all cp437 always]
  29. If I set enigma to utf8, then it works right on the ssh terminal side.
  30. But cp437 turns to puke because it's trying to convert cp437 from
  31. utf8 to cp437. The symbols get '?'.
  32. I can't detect unicode (when set to utf8), but I can detect cp437
  33. (by sending the diamonds/hearts characters).
  34. But I can't get through the enigma translation system. If only iconv worked
  35. correctly with hearts/clubs symbols! Then I wouldn't need this broken
  36. work-around code.
  37. */
  38. namespace door {
  39. /**
  40. * @brief pushback buffer for keys.
  41. *
  42. * This allows us to peek ahead and push back characters we're not interested
  43. * it. It also allows us to test the \ref Door::getkey function.
  44. */
  45. std::list<char> pushback;
  46. /**
  47. * @brief convert string to lowercase
  48. *
  49. * @param text
  50. */
  51. void to_lower(std::string &text) {
  52. transform(text.begin(), text.end(), text.begin(), ::tolower);
  53. }
  54. /**
  55. * @brief Replaces one string with another once
  56. *
  57. * Returns true if replaced.
  58. *
  59. * @param[in,out] str String to modify
  60. * @param[in] from String to search for
  61. * @param[in] to String to replace with
  62. * @return true
  63. * @return false
  64. */
  65. bool replace(std::string &str, const std::string &from, const std::string &to) {
  66. size_t start_pos = str.find(from);
  67. if (start_pos == std::string::npos)
  68. return false;
  69. str.replace(start_pos, from.length(), to);
  70. return true;
  71. }
  72. /**
  73. * @brief Replace all instances of from with to.
  74. *
  75. * @param str
  76. * @param from
  77. * @param to
  78. * @return true
  79. * @return false
  80. */
  81. bool replace(std::string &str, const char *from, const char *to) {
  82. size_t start_pos = str.find(from);
  83. if (start_pos == std::string::npos)
  84. return false;
  85. str.replace(start_pos, strlen(from), to);
  86. return true;
  87. }
  88. static bool hangup = false;
  89. /**
  90. * @brief Signal handler for detecting hangup/broken pipe
  91. *
  92. * @param signal
  93. */
  94. void sig_handler(int signal) {
  95. hangup = true;
  96. /*
  97. ofstream sigf;
  98. sigf.open("signal.log", std::ofstream::out | std::ofstream::app);
  99. sigf << "SNAP! GOT: " << signal << std::endl;
  100. sigf.close();
  101. */
  102. // 13 SIGPIPE -- ok, what do I do with this, eh?
  103. }
  104. /**
  105. * @brief Converts from one encoding to another.
  106. * Uses iconv (international conversion) API.
  107. */
  108. class IConv {
  109. iconv_t ic;
  110. public:
  111. IConv(const char *to, const char *from);
  112. ~IConv();
  113. int convert(char *input, char *output, size_t outbufsize);
  114. };
  115. /**
  116. * @brief Construct a new IConv::IConv object
  117. *
  118. * Give the encodings that you want to convert to and from.
  119. * @param to
  120. * @param from
  121. */
  122. IConv::IConv(const char *to, const char *from) : ic(iconv_open(to, from)) {}
  123. IConv::~IConv() { iconv_close(ic); }
  124. /**
  125. * @brief Calls iconv API to do the conversion.
  126. *
  127. * Buffers must be provided.
  128. * @param input
  129. * @param output
  130. * @param outbufsize
  131. * @return int
  132. */
  133. int IConv::convert(char *input, char *output, size_t outbufsize) {
  134. size_t inbufsize = strlen(input);
  135. // size_t orig_size = outbufsize;
  136. // memset(output, 0, outbufsize);
  137. // https://www.gnu.org/savannah-checkouts/gnu/libiconv/documentation/libiconv-1.15/iconv.3.html
  138. int r = iconv(ic, &input, &inbufsize, &output, &outbufsize);
  139. *output = 0;
  140. return r;
  141. }
  142. /**
  143. * @brief converter that we provide to convert from CP437 to UTF-8.
  144. * \re cp437toUnicode
  145. */
  146. static IConv converter("UTF-8", "CP437");
  147. /**
  148. * @brief Convert from CP437 to unicode.
  149. *
  150. * @param input
  151. * @param out
  152. */
  153. void cp437toUnicode(std::string input, std::string &out) {
  154. char buffer[10240];
  155. char output[16384];
  156. strcpy(buffer, input.c_str());
  157. converter.convert(buffer, output, sizeof(output));
  158. out.assign(output);
  159. }
  160. /**
  161. * @brief Convert from CP437 to unicode.
  162. *
  163. * @param input
  164. * @param out
  165. */
  166. void cp437toUnicode(const char *input, std::string &out) {
  167. char buffer[10240];
  168. char output[16384];
  169. strcpy(buffer, input);
  170. converter.convert(buffer, output, sizeof(output));
  171. out.assign(output);
  172. }
  173. /**
  174. * @brief Was unicode detected?
  175. */
  176. bool unicode = false;
  177. /**
  178. * @brief Was full CP437 detected?
  179. *
  180. * This is for full CP437 support, meaning it also supports hearts, diamonds,
  181. * spades, clubs char(3)..char(6). These are sometimes ignored by CP437
  182. * translation programs as control codes.
  183. */
  184. bool full_cp437 = false;
  185. /**
  186. * @brief Capture the output for debugging.
  187. *
  188. * This is used by the tests.
  189. */
  190. bool debug_capture = false;
  191. /**
  192. * @brief Construct a new Door:: Door object
  193. *
  194. * @param[in] dname Door name used for logfile
  195. * @param[in] argc
  196. * @param[in] argv
  197. */
  198. Door::Door(std::string dname, int argc, char *argv[])
  199. : std::ostream(this), doorname{dname},
  200. has_dropfile{false}, debugging{false}, seconds_elapsed{0},
  201. previous(COLOR::WHITE), track{true}, cx{1}, cy{1},
  202. inactivity{120}, node{1} {
  203. // Setup commandline options
  204. opt.addUsage("Door++ library by BUGZ (C) 2021");
  205. opt.addUsage("");
  206. opt.addUsage(" -h --help Displays this help");
  207. opt.addUsage(" -l --local Local Mode");
  208. opt.addUsage(" -d --dropfile [FILENAME] Load Dropfile");
  209. opt.addUsage(" -n --node N Set node number");
  210. // opt.addUsage(" -c --cp437 Force CP437");
  211. // opt.addUsage(" -b --bbsname NAME Set BBS Name");
  212. opt.addUsage(" -u --username NAME Set Username");
  213. opt.addUsage(" -t --timeleft N Set time left");
  214. opt.addUsage(" --maxtime N Set max time");
  215. opt.addUsage("");
  216. opt.setFlag("help", 'h');
  217. opt.setFlag("local", 'l');
  218. opt.setFlag("cp437", 'c');
  219. opt.setFlag("unicode");
  220. opt.setFlag("debuggering");
  221. opt.setOption("dropfile", 'd');
  222. // opt.setOption("bbsname", 'b');
  223. opt.setOption("username", 'u');
  224. opt.setOption("timeleft", 't');
  225. opt.setOption("maxtime");
  226. opt.processCommandArgs(argc, argv);
  227. if (!opt.hasOptions()) {
  228. opt.printUsage();
  229. exit(1);
  230. }
  231. if (opt.getFlag("help") || opt.getFlag('h')) {
  232. opt.printUsage();
  233. exit(1);
  234. }
  235. if (opt.getValue("username") != nullptr) {
  236. username = opt.getValue("username");
  237. handle = username;
  238. }
  239. if (opt.getFlag("debuggering")) {
  240. debugging = true;
  241. }
  242. if (opt.getValue("node") != nullptr) {
  243. node = atoi(opt.getValue("node"));
  244. }
  245. if (opt.getValue("timeleft") != nullptr) {
  246. time_left = atoi(opt.getValue("timeleft"));
  247. } else {
  248. // sensible default
  249. time_left = 25;
  250. }
  251. time_used = 0;
  252. /*
  253. if (opt.getValue("bbsname") != nullptr) {
  254. bbsname = opt.getValue("bbsname");
  255. }
  256. */
  257. std::string logFileName = dname + ".log";
  258. logf.open(logFileName.c_str(), std::ofstream::out | std::ofstream::app);
  259. parse_dropfile(opt.getValue("dropfile"));
  260. /*
  261. If the dropfile has time_left, we'll use it.
  262. Adjust time_left by maxtime value (if given).
  263. */
  264. if (opt.getValue("maxtime") != nullptr) {
  265. int maxtime = atoi(opt.getValue("maxtime"));
  266. if (time_left > maxtime) {
  267. logf << "Adjusting time from " << time_left << " to " << maxtime
  268. << std::endl;
  269. time_left = maxtime;
  270. }
  271. }
  272. if (opt.getFlag("local")) {
  273. node = 0;
  274. if (username.empty()) {
  275. uid_t uid = geteuid();
  276. struct passwd *pw;
  277. pw = getpwuid(uid);
  278. if (pw) {
  279. // Do we have realname as well?
  280. std::string name = pw->pw_gecos;
  281. size_t pos = name.find(',');
  282. username = pw->pw_name;
  283. if (pos != std::string::npos) {
  284. name = name.substr(0, pos);
  285. if (!name.empty()) {
  286. username = name;
  287. handle = pw->pw_name;
  288. }
  289. }
  290. logf << "username: " << username << " handle: " << handle << std::endl;
  291. } else {
  292. std::cout << "Local mode requires username to be set." << std::endl;
  293. opt.printUsage();
  294. exit(1);
  295. };
  296. }
  297. // local mode, make sure we have a handle -- even if it's the same as
  298. // username.
  299. if (handle.empty())
  300. handle = username;
  301. } else {
  302. // we must have a dropfile, or else!
  303. if (!has_dropfile) {
  304. std::cout << "I require a dropfile. And a shrubbery." << std::endl;
  305. opt.printUsage();
  306. exit(1);
  307. }
  308. }
  309. // Set program name
  310. log() << "Door init" << std::endl;
  311. init();
  312. // door.sys doesn't give BBS name. system_name
  313. if (!debugging) {
  314. detect_unicode_and_screen();
  315. logf << "Screen " << width << " X " << height << " unicode " << unicode
  316. << " full_cp437 " << full_cp437 << std::endl;
  317. }
  318. if (opt.getFlag("cp437")) {
  319. unicode = false;
  320. }
  321. if (opt.getFlag("unicode")) {
  322. unicode = true;
  323. }
  324. }
  325. Door::~Door() {
  326. // restore default mode
  327. // tcsetattr(STDIN_FILENO, TCSANOW, &tio_default);
  328. log() << "dtor" << std::endl;
  329. tcsetattr(STDIN_FILENO, TCOFLUSH, &tio_default);
  330. signal(SIGHUP, SIG_DFL);
  331. signal(SIGPIPE, SIG_DFL);
  332. // stop time thread
  333. stop_thread.set_value();
  334. time_thread.join();
  335. log() << "done" << std::endl;
  336. logf.close();
  337. }
  338. // https://www.tutorialspoint.com/how-do-i-terminate-a-thread-in-cplusplus11
  339. void Door::time_thread_run(std::future<void> future) {
  340. while (future.wait_for(std::chrono::milliseconds(1)) ==
  341. std::future_status::timeout) {
  342. std::this_thread::sleep_for(std::chrono::seconds(1));
  343. ++seconds_elapsed;
  344. if (seconds_elapsed % 60 == 0) {
  345. if (time_left > 0)
  346. --time_left;
  347. ++time_used;
  348. }
  349. }
  350. }
  351. void Door::init(void) {
  352. // https://viewsourcecode.org/snaptoken/kilo/02.enteringRawMode.html
  353. // https://viewsourcecode.org/snaptoken/kilo/03.rawInputAndOutput.html
  354. // ok! I didn't know about VTIME ! That's something different!
  355. // enable terminal RAW mode
  356. struct termios tio_raw;
  357. tcgetattr(STDIN_FILENO, &tio_default);
  358. tio_raw = tio_default;
  359. cfmakeraw(&tio_raw);
  360. // local terminal magic
  361. tio_raw.c_cc[VMIN] = 0;
  362. tio_raw.c_cc[VTIME] = 1;
  363. tcsetattr(STDIN_FILENO, TCSANOW, &tio_raw);
  364. startup = std::time(nullptr);
  365. signal(SIGHUP, sig_handler);
  366. signal(SIGPIPE, sig_handler);
  367. // time thread
  368. std::future<void> stop_future = stop_thread.get_future();
  369. time_thread =
  370. std::thread(&Door::time_thread_run, this, std::move(stop_future));
  371. }
  372. /**
  373. * @brief Detect unicode/CP437, and screen size.
  374. *
  375. * This sets unicode and full_cp437 flags, width and height.
  376. *
  377. * This works by clearing the screen and homing the cursor. We then output some
  378. * CP437 symbols and ask for the cursor position. We send a newline, output a
  379. * unicode symbol, and query cursor position.
  380. *
  381. * The CP437 symbols we use are specific. They are the hearts and diamonds
  382. * symbols, which are commonly mistaken for control codes. If unicode == false
  383. * and full_cp437 == true, then we do fully support CP437 on this terminal.
  384. *
  385. * For the screensize, we move the cursor down 999 and move cursor right 999 and
  386. * query position.
  387. *
  388. * On failure to detect screensize, width and height are set to 0.
  389. */
  390. void Door::detect_unicode_and_screen(void) {
  391. unicode = false;
  392. full_cp437 = false;
  393. width = 0;
  394. height = 0;
  395. if (!isatty(STDIN_FILENO)) {
  396. // https://stackoverflow.com/questions/273261/force-telnet-client-into-character-mode
  397. *this << "\377\375\042\377\373\001"; // fix telnet client
  398. }
  399. // maybe I need to be trying to detect cp437 instead of trying to detect
  400. // unicde!
  401. *this << "\x1b[0;30;40m\x1b[2J\x1b[H"; // black on black, clrscr, go home
  402. *this << "\x03\x04" // hearts and diamonds does CP437 work?
  403. << "\x1b[6n"; // cursor pos
  404. *this << door::nl << "\u2615"
  405. << "\x1b[6n"; // hot beverage + cursor pos
  406. *this << "\x1b[999C\x1b[999B\x1b[6n"; // goto end of screen + cursor pos
  407. *this << reset << "\x1b[2J\x1b[H"; // reset, cls, go home
  408. this->flush();
  409. usleep(1000 * 1000);
  410. if (haskey()) {
  411. char buffer[101];
  412. int len;
  413. len = read(STDIN_FILENO, &buffer, sizeof(buffer) - 1);
  414. // logf << "read " << len << std::endl;
  415. if (len > 0) {
  416. buffer[len] = 0;
  417. int x;
  418. #ifdef DEBUG_OUTPUT
  419. for (x = 0; x < len; x++) {
  420. if (buffer[x] < 0x20)
  421. logf << std::hex << (int)buffer[x] << " ";
  422. else
  423. logf << buffer[x] << " ";
  424. }
  425. #endif
  426. for (x = 0; x < len; x++) {
  427. if (buffer[x] == 0)
  428. buffer[x] = ' ';
  429. }
  430. #ifdef DEBUG_OUTPUT
  431. logf << std::endl;
  432. logf << "BUFFER [" << (char *)buffer << "]" << std::endl;
  433. #endif
  434. // log detection results
  435. if (1) {
  436. std::string cleanbuffer = buffer;
  437. std::string esc = "\x1b";
  438. std::string esc_text = "^[";
  439. while (replace(cleanbuffer, esc, esc_text)) {
  440. };
  441. logf << "BUFFER [" << cleanbuffer << "]" << std::endl;
  442. }
  443. // 1;3R required on David's machine. I'm not sure why.
  444. // 1;3R also happens under VSCodium.
  445. // 1;4R is what I get from syncterm.
  446. if (((strstr(buffer, "1;1R") != nullptr) or
  447. (strstr(buffer, "1;3R") != nullptr)) and
  448. ((strstr(buffer, "2;2R") != nullptr) or
  449. (strstr(buffer, "2;3R") != nullptr))) {
  450. unicode = true;
  451. log() << "unicode enabled \u2615" << std::endl; // "U0001f926");
  452. } else {
  453. if (strstr(buffer, "1;3R") != nullptr) {
  454. full_cp437 = true;
  455. }
  456. }
  457. // Get the terminal screen size
  458. char *cp;
  459. cp = strrchr(buffer, '\x1b');
  460. if (cp != nullptr) {
  461. cp++;
  462. if (*cp == '[') {
  463. cp++;
  464. height = atoi(cp);
  465. cp = strchr(cp, ';');
  466. if (cp != nullptr) {
  467. cp++;
  468. width = atoi(cp);
  469. if (width > 900) {
  470. // something went wrong
  471. width = 0;
  472. height = 0;
  473. }
  474. } else {
  475. height = 0;
  476. }
  477. }
  478. }
  479. }
  480. } else {
  481. logf << "FAIL-WHALE, no response to terminal getposition." << std::endl;
  482. }
  483. }
  484. /**
  485. * @brief Load dropfile into dropfilelines and parse.
  486. *
  487. * Load dropfile and parse door.sys and door32.sys.
  488. *
  489. * @param[in] filepath Dropfile path and filename
  490. */
  491. void Door::parse_dropfile(const char *filepath) {
  492. if (filepath == nullptr)
  493. return;
  494. // Ok, parse file here...
  495. std::ifstream file(filepath);
  496. if (file.fail()) {
  497. log() << "Failed to open " << filepath << std::endl;
  498. return;
  499. }
  500. std::string line;
  501. while (std::getline(file, line)) {
  502. // These are "DOS" files. Remove trailing \r.
  503. if (!line.empty() && line[line.size() - 1] == '\r')
  504. line.erase(line.size() - 1);
  505. dropfilelines.push_back(line);
  506. }
  507. file.close();
  508. std::string filename;
  509. {
  510. // converting const char * to char * for basename.
  511. char *temp = strdup(filepath);
  512. filename = basename(temp);
  513. free(temp);
  514. }
  515. to_lower(filename);
  516. // for now, just door.sys.
  517. if (filename == "door.sys") {
  518. // Ok, parse away!
  519. node = atoi(dropfilelines[3].c_str());
  520. username = dropfilelines[9];
  521. location = dropfilelines[10];
  522. time_left = atoi(dropfilelines[18].c_str());
  523. sysop = dropfilelines[34];
  524. handle = dropfilelines[35];
  525. } else {
  526. if (filename == "door32.sys") {
  527. // https://raw.githubusercontent.com/NuSkooler/ansi-bbs/master/docs/dropfile_formats/door32_sys.txt
  528. // dropfilelines[0] = Comm type (0=local, 1=serial, 2=telnet)
  529. // dropfilelines[1] = Comm or Socket handle
  530. // dropfilelines[2] = BaudRate
  531. // dropfilelines[3] = BBS Software Version
  532. username = dropfilelines[4];
  533. handle = dropfilelines[5];
  534. time_left = atoi(dropfilelines[6].c_str());
  535. // dropfilelines[7] = Emulation (0=Ascii, 1=ANSI, .. or above = ANSI)
  536. node = atoi(dropfilelines[8].c_str());
  537. } else {
  538. std::string msg = "Unknown dropfile: ";
  539. msg += filename;
  540. log() << msg << std::endl;
  541. *this << msg << std::endl;
  542. exit(2);
  543. }
  544. }
  545. log() << "node:" << node << " username: " << username << " handle: " << handle
  546. << " time: " << time_left << std::endl;
  547. has_dropfile = true;
  548. }
  549. /**
  550. * @brief Give ofstream handle for logging
  551. *
  552. * This appends the current date/time stamp into the logfile, and returns a
  553. * reference.
  554. *
  555. * Example:
  556. *
  557. * door.log() << "Something bad just happened." << std::endl;
  558. *
  559. * @return ofstream&
  560. */
  561. ofstream &Door::log(void) {
  562. std::time_t t = std::time(nullptr);
  563. std::tm tm = *std::localtime(&t);
  564. logf << std::put_time(&tm, "%c ") << node << " " << username << " ";
  565. return logf;
  566. }
  567. /**
  568. * @brief Are there any keys in STDIN?
  569. *
  570. * This uses select to check if we have received any keys. This does not use
  571. * pushback.
  572. *
  573. * If HANGUP, OUTOFTIME, return true
  574. *
  575. * @return true
  576. * @return false
  577. */
  578. bool Door::haskey(void) {
  579. fd_set socket_set;
  580. struct timeval tv;
  581. int select_ret = -1;
  582. if (hangup)
  583. return true; // HANGUP;
  584. if (time_left < 2)
  585. return true; // OUTOFTIME;
  586. while (select_ret == -1) {
  587. FD_ZERO(&socket_set);
  588. FD_SET(STDIN_FILENO, &socket_set);
  589. tv.tv_sec = 0;
  590. tv.tv_usec = 1;
  591. select_ret = select(STDIN_FILENO + 1, &socket_set, NULL, NULL, &tv);
  592. if (select_ret == -1) {
  593. if (errno == EINTR)
  594. continue;
  595. log() << "hangup detected" << std::endl;
  596. hangup = true;
  597. return true; // HANGUP;
  598. }
  599. if (select_ret == 0)
  600. return false;
  601. }
  602. return true;
  603. }
  604. /**
  605. * @brief low level read key.
  606. *
  607. * Returns key, or
  608. * -1 no key available/timeout
  609. * -2 read error/hang up
  610. * -3 out of time
  611. *
  612. * @return signed int
  613. */
  614. signed int Door::getch(void) {
  615. fd_set socket_set;
  616. struct timeval tv;
  617. int select_ret = -1;
  618. int recv_ret;
  619. char key;
  620. if (door::hangup)
  621. return HANGUP;
  622. if (time_left < 2)
  623. return OUTOFTIME;
  624. while (select_ret == -1) {
  625. FD_ZERO(&socket_set);
  626. FD_SET(STDIN_FILENO, &socket_set);
  627. // This delay isn't long enough for QModem in a DOSBOX.
  628. // doorway mode arrow keys aren't always caught.
  629. tv.tv_sec = 0;
  630. tv.tv_usec = 100;
  631. // tv.tv_usec = 500;
  632. select_ret = select(STDIN_FILENO + 1, &socket_set, NULL, NULL, &tv);
  633. // select(STDIN_FILENO + 1, &socket_set, NULL, NULL, bWait ? NULL : &tv);
  634. if (select_ret == -1) {
  635. if (errno == EINTR)
  636. continue;
  637. log() << "hangup detected" << std::endl;
  638. door::hangup = true;
  639. return HANGUP;
  640. }
  641. if (select_ret == 0)
  642. return TIMEOUT;
  643. }
  644. recv_ret = read(STDIN_FILENO, &key, 1);
  645. if (recv_ret != 1) {
  646. // possibly log this.
  647. log() << "hangup" << std::endl;
  648. hangup = true;
  649. return HANGUP;
  650. }
  651. // debug weird keys/layouts.
  652. // log() << "read " << std::hex << (int)key << std::endl;
  653. return key;
  654. }
  655. /**
  656. * Call low level getch if the pushback buffer is empty.
  657. *
  658. * This allows testing of the \ref Door::getkey function.
  659. *
  660. * @return signed int
  661. */
  662. signed int Door::getkey_or_pushback(void) {
  663. signed int c;
  664. if (!door::pushback.empty()) {
  665. c = door::pushback.front();
  666. door::pushback.pop_front();
  667. return c;
  668. }
  669. return getch();
  670. }
  671. /**
  672. * @brief Get a key routine.
  673. *
  674. * This returns the key received, or XKEY_* values for function keys, etc.
  675. * If return < 0:
  676. * -1 timeout/no key
  677. * -2 hangup
  678. * -3 out of time
  679. *
  680. * @return signed int
  681. */
  682. signed int Door::getkey(void) {
  683. signed int c, c2;
  684. c = getkey_or_pushback();
  685. if (c < 0)
  686. return c;
  687. /*
  688. We get 0x0d 0x00 for [Enter] (Syncterm)
  689. From David's syncterm, I'm getting 0x0d 0x0a.
  690. This strips out the null.
  691. */
  692. if (c == 0x0d) {
  693. c2 = getkey_or_pushback();
  694. if ((c2 != 0) and (c2 >= 0) and (c2 != 0x0a)) {
  695. log() << "got " << (int)c2 << " so stuffing it into unget buffer."
  696. << std::endl;
  697. door::pushback.push_front(c2);
  698. }
  699. return c;
  700. }
  701. if (c == 0) {
  702. // possibly "doorway mode"
  703. int tries = 0;
  704. c2 = getkey_or_pushback();
  705. while (c2 < 0) {
  706. if (tries > 7) {
  707. log() << "ok, got " << c2 << " and " << tries << " so returning 0x00!"
  708. << std::endl;
  709. return c;
  710. }
  711. c2 = getkey_or_pushback();
  712. ++tries;
  713. }
  714. if (tries > 0) {
  715. log() << "tries " << tries << std::endl;
  716. }
  717. switch (c2) {
  718. case 0x50:
  719. return XKEY_DOWN_ARROW;
  720. case 0x48:
  721. return XKEY_UP_ARROW;
  722. case 0x4b:
  723. return XKEY_LEFT_ARROW;
  724. case 0x4d:
  725. return XKEY_RIGHT_ARROW;
  726. case 0x47:
  727. return XKEY_HOME;
  728. case 0x4f:
  729. return XKEY_END;
  730. case 0x49:
  731. return XKEY_PGUP;
  732. case 0x51:
  733. return XKEY_PGDN;
  734. case 0x3b:
  735. return XKEY_F1;
  736. case 0x3c:
  737. return XKEY_F2;
  738. case 0x3d:
  739. return XKEY_F3;
  740. case 0x3e:
  741. return XKEY_F4;
  742. case 0x3f:
  743. return XKEY_F5;
  744. case 0x40:
  745. return XKEY_F6;
  746. case 0x41:
  747. return XKEY_F7;
  748. case 0x42:
  749. return XKEY_F8;
  750. case 0x43:
  751. return XKEY_F9;
  752. case 0x44:
  753. return XKEY_F10;
  754. /*
  755. case 0x45:
  756. return XKEY_F11;
  757. case 0x46:
  758. return XKEY_F12;
  759. */
  760. case 0x52:
  761. return XKEY_INSERT;
  762. case 0x53:
  763. return XKEY_DELETE;
  764. }
  765. logf << "\r\nDEBUG:\r\n0x00 + 0x" << std::hex << (int)c2;
  766. logf << "\r\n";
  767. logf.flush();
  768. }
  769. if (c == 0x1b) {
  770. // possible extended key
  771. c2 = getkey_or_pushback();
  772. if (c2 < 0) {
  773. // nope, just plain ESC key
  774. return c;
  775. }
  776. // consume extended key values int extended buffer
  777. char extended[16];
  778. unsigned int pos = 0;
  779. extended[pos] = (char)c2;
  780. extended[pos + 1] = 0;
  781. pos++;
  782. while ((pos < sizeof(extended) - 1) and ((c2 = getch()) >= 0)) {
  783. // special case here where I'm sending out cursor location requests
  784. // and the \x1b[X;YR strings are getting buffered.
  785. if (c2 == 0x1b) {
  786. door::pushback.push_front(c2);
  787. break;
  788. }
  789. extended[pos] = (char)c2;
  790. extended[pos + 1] = 0;
  791. pos++;
  792. }
  793. // convert extended buffer to special key
  794. if (extended[0] == '[') {
  795. switch (extended[1]) {
  796. case 'A':
  797. return XKEY_UP_ARROW;
  798. case 'B':
  799. return XKEY_DOWN_ARROW;
  800. case 'C':
  801. return XKEY_RIGHT_ARROW;
  802. case 'D':
  803. return XKEY_LEFT_ARROW;
  804. case 'H':
  805. return XKEY_HOME;
  806. case 'F':
  807. return XKEY_END; // terminal
  808. case 'K':
  809. return XKEY_END;
  810. case 'U':
  811. return XKEY_PGUP;
  812. case 'V':
  813. return XKEY_PGDN;
  814. case '@':
  815. return XKEY_INSERT;
  816. }
  817. if (extended[pos - 1] == '~') {
  818. // \x1b[digits~
  819. int number = atoi(extended + 1);
  820. switch (number) {
  821. case 2:
  822. return XKEY_INSERT; // terminal
  823. case 3:
  824. return XKEY_DELETE; // terminal
  825. case 5:
  826. return XKEY_PGUP; // terminal
  827. case 6:
  828. return XKEY_PGDN; // terminal
  829. case 15:
  830. return XKEY_F5; // terminal
  831. case 17:
  832. return XKEY_F6; // terminal
  833. case 18:
  834. return XKEY_F7; // terminal
  835. case 19:
  836. return XKEY_F8; // terminal
  837. case 20:
  838. return XKEY_F9; // terminal
  839. case 21:
  840. return XKEY_F10; // terminal
  841. case 23:
  842. return XKEY_F11;
  843. case 24:
  844. return XKEY_F12; // terminal
  845. }
  846. }
  847. }
  848. if (extended[0] == 'O') {
  849. switch (extended[1]) {
  850. case 'P':
  851. return XKEY_F1;
  852. case 'Q':
  853. return XKEY_F2;
  854. case 'R':
  855. return XKEY_F3;
  856. case 'S':
  857. return XKEY_F4;
  858. case 't':
  859. return XKEY_F5; // syncterm
  860. }
  861. }
  862. // unknown -- This needs to be logged
  863. logf << "\r\nDEBUG:\r\nESC + ";
  864. for (unsigned int x = 0; x < pos; x++) {
  865. char z = extended[x];
  866. if (iscntrl(z)) {
  867. logf << (int)z << " ";
  868. } else {
  869. logf << "'" << (char)z << "'"
  870. << " ";
  871. };
  872. }
  873. logf << "\r\n";
  874. logf.flush();
  875. return XKEY_UNKNOWN;
  876. }
  877. return c;
  878. }
  879. /*
  880. The following code will wait for 1.5 second:
  881. #include <sys/select.h>
  882. #include <sys/time.h>
  883. #include <unistd.h>`
  884. int main() {
  885. struct timeval t;
  886. t.tv_sec = 1;
  887. t.tv_usec = 500000;
  888. select(0, NULL, NULL, NULL, &t);
  889. }
  890. */
  891. /**
  892. * @brief Waits secs seconds for a keypress.
  893. *
  894. * returns key, or -1 on timeout (seconds passed).
  895. * -2 hangup
  896. * -3 out of time
  897. *
  898. * @param secs
  899. * @return signed int
  900. */
  901. signed int Door::sleep_key(int secs) {
  902. fd_set socket_set;
  903. struct timeval tv;
  904. int select_ret = -1;
  905. /*
  906. int recv_ret;
  907. char key;
  908. */
  909. if (hangup)
  910. return HANGUP;
  911. if (time_left < 2)
  912. return OUTOFTIME;
  913. while (select_ret == -1) {
  914. FD_ZERO(&socket_set);
  915. FD_SET(STDIN_FILENO, &socket_set);
  916. tv.tv_sec = secs;
  917. tv.tv_usec = 0;
  918. select_ret = select(STDIN_FILENO + 1, &socket_set, NULL, NULL, &tv);
  919. // select(STDIN_FILENO + 1, &socket_set, NULL, NULL, bWait ? NULL : &tv);
  920. if (select_ret == -1) {
  921. if (errno == EINTR)
  922. continue;
  923. hangup = true;
  924. log() << "hangup detected" << std::endl;
  925. return HANGUP;
  926. }
  927. if (select_ret == 0)
  928. return TIMEOUT;
  929. }
  930. return getkey();
  931. }
  932. /**
  933. * @brief Waits miliseconds for a keypress.
  934. *
  935. * returns key, or -1 on timeout (seconds passed).
  936. * -2 hangup
  937. * -3 out of time
  938. *
  939. * @param msecs
  940. * @return signed int
  941. */
  942. signed int Door::sleep_ms_key(int msecs) {
  943. fd_set socket_set;
  944. struct timeval tv;
  945. int select_ret = -1;
  946. /*
  947. int recv_ret;
  948. char key;
  949. */
  950. if (hangup)
  951. return HANGUP;
  952. if (time_left < 2)
  953. return OUTOFTIME;
  954. while (select_ret == -1) {
  955. FD_ZERO(&socket_set);
  956. FD_SET(STDIN_FILENO, &socket_set);
  957. tv.tv_sec = 0;
  958. tv.tv_usec = msecs * 1000;
  959. select_ret = select(STDIN_FILENO + 1, &socket_set, NULL, NULL, &tv);
  960. // select(STDIN_FILENO + 1, &socket_set, NULL, NULL, bWait ? NULL : &tv);
  961. if (select_ret == -1) {
  962. if (errno == EINTR)
  963. continue;
  964. hangup = true;
  965. log() << "hangup detected" << std::endl;
  966. return HANGUP;
  967. }
  968. if (select_ret == 0)
  969. return TIMEOUT;
  970. }
  971. return getkey();
  972. }
  973. /**
  974. * @brief Input a string of requested max length.
  975. *
  976. * This first sends out max number of spaces, and max number of backspaces. This
  977. * will setup the input area. (If you set a background color of blue, this
  978. * would allow that to be seen by the user.)
  979. *
  980. * It handles input, backspaces / deleting the characters / enter input and
  981. * timeout/hangup/out of time.
  982. *
  983. * @param max
  984. * @return std::string
  985. */
  986. std::string Door::input_string(int max) {
  987. std::string input;
  988. // draw the input area.
  989. *this << std::string(max, ' ');
  990. *this << std::string(max, '\x08');
  991. int c;
  992. while (true) {
  993. c = sleep_key(inactivity);
  994. if (c < 0) {
  995. input.clear();
  996. return input;
  997. }
  998. if (c > 0x1000)
  999. continue;
  1000. if (isprint(c)) {
  1001. if (int(input.length()) < max) {
  1002. *this << char(c);
  1003. input.append(1, c);
  1004. } else {
  1005. // bell
  1006. *this << '\x07';
  1007. }
  1008. } else {
  1009. switch (c) {
  1010. case 0x08:
  1011. case 0x7f:
  1012. if (input.length() > 0) {
  1013. *this << "\x08 \x08";
  1014. // this->flush();
  1015. input.erase(input.length() - 1);
  1016. };
  1017. break;
  1018. case 0x0d:
  1019. return input;
  1020. }
  1021. }
  1022. }
  1023. }
  1024. /**
  1025. * @brief Get one of these keys
  1026. *
  1027. * returns char, or < 0 if timeout.
  1028. *
  1029. * @param keys
  1030. * @return char or < 0
  1031. */
  1032. int Door::get_one_of(const char *keys) {
  1033. int c;
  1034. while (true) {
  1035. c = sleep_key(inactivity);
  1036. if (c < 0)
  1037. return c;
  1038. if (c > 0x1000)
  1039. continue;
  1040. const char *key = strchr(keys, (char)toupper(c));
  1041. if (key != nullptr) {
  1042. return *key;
  1043. }
  1044. *this << '\x07';
  1045. }
  1046. return c;
  1047. }
  1048. /**
  1049. * Take given buffer and output it.
  1050. *
  1051. * If debug_capture is enabled, we save everything to debug_buffer.
  1052. * This is used by the tests.
  1053. *
  1054. * @param s const char *
  1055. * @param n std::streamsize
  1056. * @return std::streamsize
  1057. */
  1058. std::streamsize Door::xsputn(const char *s, std::streamsize n) {
  1059. if (debug_capture) {
  1060. debug_buffer.append(s, n);
  1061. } else {
  1062. static std::string buffer;
  1063. buffer.append(s, n);
  1064. // setp(&(*buffer.begin()), &(*buffer.end()));
  1065. if (!hangup) {
  1066. std::cout << buffer;
  1067. std::cout.flush();
  1068. }
  1069. // Tracking character position could be a problem / local terminal unicode.
  1070. if (track)
  1071. cx += n;
  1072. buffer.clear();
  1073. }
  1074. return n;
  1075. }
  1076. /**
  1077. * Stores a character into the buffer.
  1078. * This does still use the buffer.
  1079. * @todo Replace this also with a direct call to od_disp_emu.
  1080. *
  1081. * @param c char
  1082. * @return int
  1083. */
  1084. int Door::overflow(int c) {
  1085. if (debug_capture) {
  1086. debug_buffer.append(1, (char)c);
  1087. } else {
  1088. if (!hangup) {
  1089. std::cout << (char)c;
  1090. std::cout.flush();
  1091. }
  1092. }
  1093. if (track)
  1094. cx++;
  1095. // setp(&(*buffer.begin()), &(*buffer.end()));
  1096. return c;
  1097. }
  1098. /**
  1099. * Construct a new Color Output:: Color Output object
  1100. * We default to BLACK/BLACK (not really a valid color),
  1101. * pos=0 and len=0.
  1102. */
  1103. ColorOutput::ColorOutput() : c(COLOR::BLACK, COLOR::BLACK) {
  1104. pos = 0;
  1105. len = 0;
  1106. }
  1107. /**
  1108. * Reset pos and len to 0.
  1109. */
  1110. void ColorOutput::reset(void) {
  1111. pos = 0;
  1112. len = 0;
  1113. }
  1114. /**
  1115. * Construct a new Render:: Render object
  1116. *
  1117. * Render consists of constant text,
  1118. * and vector of ColorOutput
  1119. *
  1120. * @param txt Text
  1121. */
  1122. Render::Render(const std::string txt) : text{txt} {}
  1123. /**
  1124. * Output the Render.
  1125. *
  1126. * This consists of going through the vector, getting
  1127. * the fragment (from pos and len), and outputting the
  1128. * color and fragment.
  1129. *
  1130. * @param os
  1131. */
  1132. void Render::output(std::ostream &os) {
  1133. for (auto out : outputs) {
  1134. std::string fragment = text.substr(out.pos, out.len);
  1135. os << out.c << fragment;
  1136. }
  1137. }
  1138. /**
  1139. * Create render output.
  1140. *
  1141. * Call this for each section you want to colorize.
  1142. *
  1143. * @param color
  1144. * @param len
  1145. */
  1146. void Render::append(ANSIColor color, int len) {
  1147. if (outputs.empty()) {
  1148. ColorOutput co;
  1149. co.c = color;
  1150. co.pos = 0;
  1151. co.len = len;
  1152. outputs.push_back(co);
  1153. return;
  1154. }
  1155. ColorOutput &current = outputs.back();
  1156. if (current.c == color) {
  1157. current.len += len;
  1158. return;
  1159. }
  1160. // Ok, new entry
  1161. // beware the unicode text
  1162. ColorOutput co;
  1163. co.pos = current.pos + current.len;
  1164. co.c = color;
  1165. co.len = len;
  1166. outputs.push_back(co);
  1167. }
  1168. /**
  1169. * Construct a new Clrscr:: Clrscr object
  1170. *
  1171. * This is used to clear the screen.
  1172. */
  1173. Clrscr::Clrscr() {}
  1174. /**
  1175. * Clear the screen using ANSI codes.
  1176. *
  1177. * Not all systems home the cursor after clearing the screen.
  1178. * We automatically home the cursor as well.
  1179. *
  1180. * @param os std::ostream&
  1181. * @param clr const Clrscr&
  1182. * @return std::ostream&
  1183. */
  1184. std::ostream &operator<<(std::ostream &os, const Clrscr &clr) {
  1185. Door *d = dynamic_cast<Door *>(&os);
  1186. if (d != nullptr) {
  1187. d->track = false;
  1188. *d << "\x1b[2J"
  1189. "\x1b[H";
  1190. d->cx = 1;
  1191. d->cy = 1;
  1192. d->track = true;
  1193. } else {
  1194. os << "\x1b[2J"
  1195. "\x1b[H";
  1196. }
  1197. return os;
  1198. }
  1199. Clrscr cls;
  1200. /**
  1201. * This is used to issue NL+CR
  1202. *
  1203. */
  1204. NewLine::NewLine() {}
  1205. /**
  1206. * Output Newline + CarriageReturn
  1207. * @param os std::ostream
  1208. * @param nl const NewLine
  1209. * @return std::ostream&
  1210. */
  1211. std::ostream &operator<<(std::ostream &os, const NewLine &nl) {
  1212. Door *d = dynamic_cast<Door *>(&os);
  1213. if (d != nullptr) {
  1214. d->track = false;
  1215. *d << "\r\n";
  1216. d->cx = 1;
  1217. d->cy++;
  1218. d->track = true;
  1219. } else {
  1220. os << "\r\n";
  1221. };
  1222. return os;
  1223. }
  1224. NewLine nl;
  1225. /**
  1226. * Construct a new Goto:: Goto object
  1227. *
  1228. * @param xpos
  1229. * @param ypos
  1230. */
  1231. Goto::Goto(int xpos, int ypos) {
  1232. x = xpos;
  1233. y = ypos;
  1234. }
  1235. void Goto::set(int xpos, int ypos) {
  1236. x = xpos;
  1237. y = ypos;
  1238. }
  1239. /**
  1240. * Output the ANSI codes to position the cursor to the given y,x position.
  1241. *
  1242. * @todo Optimize the ANSI goto string output.
  1243. * @todo Update the Door object so it know where the cursor
  1244. * is positioned.
  1245. *
  1246. * @param os std::ostream
  1247. * @param g const Goto
  1248. * @return std::ostream&
  1249. */
  1250. std::ostream &operator<<(std::ostream &os, const Goto &g) {
  1251. Door *d = dynamic_cast<Door *>(&os);
  1252. if (d != nullptr) {
  1253. d->track = false;
  1254. *d << "\x1b[";
  1255. if (g.y > 1)
  1256. *d << std::to_string(g.y);
  1257. if (g.x > 1) {
  1258. os << ";";
  1259. *d << std::to_string(g.x);
  1260. }
  1261. *d << "H";
  1262. d->cx = g.x;
  1263. d->cy = g.y;
  1264. d->track = true;
  1265. } else {
  1266. os << "\x1b[" << std::to_string(g.y) << ";" << std::to_string(g.x) << "H";
  1267. };
  1268. return os;
  1269. }
  1270. /**
  1271. * @brief ANSI Save Cursor position command.
  1272. */
  1273. const char SaveCursor[] = "\x1b[s";
  1274. /**
  1275. * @brief ANSI Restore Cursor position command.
  1276. */
  1277. const char RestoreCursor[] = "\x1b[u";
  1278. // EXAMPLES
  1279. /// BlueYellow Render example function
  1280. renderFunction rBlueYellow = [](const std::string &txt) -> Render {
  1281. Render r(txt);
  1282. ANSIColor blue(COLOR::BLUE, ATTR::BOLD);
  1283. ANSIColor cyan(COLOR::YELLOW, ATTR::BOLD);
  1284. for (char const &c : txt) {
  1285. if (isupper(c))
  1286. r.append(blue);
  1287. else
  1288. r.append(cyan);
  1289. }
  1290. return r;
  1291. };
  1292. } // namespace door