/* Terminal tracking Actually, I believe I only really need to track the color information. Everything else, I'm not sure I really care about. (NNY!) */ #include "terminal.h" #include "utils.h" #include "zf_log.h" #include #include #include // snprintf #include #include #include Terminal::Terminal() { this->init(); } Terminal::Terminal(const Terminal &old) : saved_cursor_position(old.saved_cursor_position) { posx = old.posx; posy = old.posy; in_ansi = old.in_ansi; fgcolor = old.fgcolor; bgcolor = old.bgcolor; status = old.status; ansi = old.ansi; } Terminal &Terminal::operator=(Terminal &rhs) { posx = rhs.posx; posy = rhs.posy; in_ansi = rhs.in_ansi; fgcolor = rhs.fgcolor; bgcolor = rhs.bgcolor; status = rhs.status; ansi = rhs.ansi; saved_cursor_position = rhs.saved_cursor_position; return *this; } void Terminal::init(void) { this->posx = this->posy = 0; this->in_ansi = 0; this->fgcolor = 7; this->bgcolor = 0; this->status = 0; this->saved_cursor_position.clear(); this->dcs_map = 0x0b; } int Terminal::getx(void) { return posx; } int Terminal::gety(void) { return posy; } int Terminal::getstatus(void) { return status; } int Terminal::inANSI(void) { return in_ansi; } int Terminal::fg(void) { return fgcolor; } int Terminal::bg(void) { return bgcolor; } bool Terminal::ansiempty(void) { return ansi.empty(); } int Terminal::dcs(void) { return dcs_map; } void Terminal::ansi_color(int color) { // ZF_LOGV("ansi_color(%d)", color); if (color == 0) { this->status = 0; this->fgcolor = 7; this->bgcolor = 0; return; } if ((color == 1) || (color == 2) || (color == 3) || (color == 4) || (color == 5)) { this->status = color; return; } if ((color >= 30) && (color <= 37)) { this->fgcolor = color - 30; return; } if ((color >= 40) && (color <= 47)) { this->bgcolor = color - 40; return; } if (color == 39) { // default fg color this->fgcolor = 7; return; } if (color == 49) { // default bg color this->bgcolor = 0; return; } ZF_LOGD("ansi_color( %d ) is unknown to me.", color); } std::string Terminal::color_restore(void) { std::ostringstream oss; // possible optimize: If fg is 7, don't set (0 reset does that), if bg is 0, // don't set (0 does that) if (this->status == 0) { oss << "\x1b[0;" << this->fgcolor + 30 << ";" << this->bgcolor + 40 << "m"; } else { oss << "\x1b[0;" << this->status << ";" << this->fgcolor + 30 << ";" << this->bgcolor + 40 << "m"; } std::string buffer = oss.str(); return buffer; } Terminal::ANSI_TYPE Terminal::ansi_code(std::string ansi) { std::string::iterator cp = ansi.begin(); std::string::iterator last = ansi.end() - 1; int number, number2; if (*cp == '\x1b') { ++cp; // Ok, that's expected. if (*cp == '[') { ++cp; // https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences switch (*last) { case 'A': // cursor up if (cp == last) { number = 1; } else { number = std::stoi(std::string(cp, last)); if (number < 1) { ZF_LOGD("console_ansi( %s ): number error (%d)", repr(ansi.c_str()), number); number = 1; } }; this->posy -= number; if (this->posy < 0) { this->posy = 0; ZF_LOGD( "console_ansi( %s ): attempt to move above top of screen (%d)", repr(ansi.c_str()), number); } return CURSOR; case 'B': // cursor down if (cp == last) { number = 1; } else { number = std::stoi(std::string(cp, last)); if (number < 1) { ZF_LOGD("console_ansi( %s ): number error (%d)", repr(ansi.c_str()), number); number = 1; } }; this->posy += number; // check range/"scroll" return CURSOR; case 'C': // cursor forward if (cp == last) { number = 1; } else { number = std::stoi(std::string(cp, last)); if (number < 1) { ZF_LOGD("console_ansi( %s ): number error (%d)", repr(ansi.c_str()), number); number = 1; } }; this->posx += number; // Well. According to the "spec", the screen limits are hard // If the cursor is already at the edge of the screen, this has no // effect. ^ This is *NOT* how any ANSI BBS terminal program acts! while (this->posx > 79) { this->posy++; // check range/"scroll" this->posx -= 79; } return CURSOR; case 'D': // cursor backwards if (cp == last) { number = 1; } else { number = std::stoi(std::string(cp, last)); if (number < 1) { ZF_LOGD("console_ansi( %s ): number error (%d)", repr(ansi.c_str()), number); number = 0; } }; this->posx -= number; // Well. According to the "spec", the screen limits are hard // If the cursor is already at the edge of the screen, this has no // effect. ^ This is *NOT* how any ANSI BBS terminal program acts! while (this->posx < 0) { this->posy--; if (this->posy < 0) { this->posy = 0; } this->posx += 79; } return CURSOR; case 'H': // cursor position if (cp == last) { // Missing first and last number = 1; number2 = 1; } else { if (*cp == ';') { // Missing first number number = 1; ++cp; if (cp == last) { // missing 2nd number as well? number2 = 1; } else { number2 = std::stoi(std::string(cp, last)); } } else { // Ok, find the first number number = std::stoi(std::string(cp, last)); // covert iterator to position std::size_t pos = ansi.find(';', std::distance(ansi.begin(), cp)); if (pos == std::string::npos) { // Missing 2nd number number2 = 1; } else { // 2nd number found, maybe. cp = ansi.begin() + pos; ++cp; if (cp == last) { number2 = 1; } else { number2 = std::stoi(std::string(cp, last)); } } } } // Our positions start at zero, not one. this->posy = number - 1; this->posx = number2 - 1; return CURSOR; case 'J': // clear if (cp == last) { number = 0; } else { number = std::stoi(std::string(cp, last)); }; // clears ... part of the screen. if (number == 2) { this->posx = 0; this->posy = 0; }; return CLEAR; case 'K': // clear line if (cp == last) { number = 0; } else { number = std::stoi(std::string(cp, last)); }; return CLEAR; case 'm': // color if (cp == last) { // nothing given, default to 0. number = 0; ansi_color(number); } else { while (cp != last) { number = std::stoi(std::string(cp, last)); ansi_color(number); ++cp; while ((cp != last) && (isdigit(*cp))) { ++cp; }; if (cp != last) { if (*cp == ';') { cp++; } } } } return COLOR; case 's': // save position saved_cursor_position.push_back(std::make_pair(this->posx, this->posy)); return CURSOR; case 'u': // restore position if (saved_cursor_position.empty()) { ZF_LOGE("Restore cursor position from empty history."); } else { std::pair pos = saved_cursor_position.back(); this->posx = pos.first; this->posy = pos.second; saved_cursor_position.pop_back(); } return CURSOR; case 't': case 'r': case 'h': case '!': // These are ones that I don't care about. case 'n': // This is terminal detect -- give me cursor position return OTHER; default: // unsure -- possibly not important ZF_LOGD("console_ansi( %s ): ???", repr(ansi.c_str())); return OTHER; } } } ZF_LOGD("console_ansi( %s ) : ???", repr(ansi.c_str())); return OTHER; } Terminal::termchar Terminal::putchar(char ch) { Terminal::termchar tc; if (this->in_ansi) { // Ok, append this char this->ansi.append(1, ch); // DCS (Designate Character Set) if (ansi[1] == '(') { if (ansi.size() == 3) { // End DCS switch (ansi[2]) { case '0': dcs_map = 0; break; case 'B': dcs_map = 0x0b; break; default: ZF_LOGE("Unknown DCS Mode ^ESC(%c Selected!", ansi[2]); // Use our default, but we know it's wrong! dcs_map = 0x0b; break; } this->in_ansi = 0; this->ansi.clear(); tc.in_ansi = 1; tc.ansi = DCS; return tc; } } if (isalpha(ch)) { // Ok! end of ANSI code, process it. tc.ansi = ansi_code(this->ansi); this->in_ansi = 0; this->ansi.clear(); tc.in_ansi = 1; return tc; } tc.ansi = START; tc.in_ansi = 1; return tc; } else { tc.ansi = START; tc.in_ansi = 0; if (ch == '\x1b') { this->ansi.append(1, ch); this->in_ansi = 1; tc.in_ansi = 1; return tc; } // should I try reporting MOTION non-ANSI ? if (ch == '\r') { // Carriage return this->posx = 0; return tc; } if (ch == '\n') { this->posy++; // check range/"scroll" return tc; } if (ch == '\b') { // Backspace. if (this->posx > 0) { this->posx--; } return tc; } if (ch == '\f') { // form feed // treat as clear screen this->posx = 0; this->posy = 0; return tc; } /* I don't believe that anything else can possibly be here, other then an actual printable character. So! FUTURE: Store the screen text + colors */ this->posx++; if (this->posx > 79) { this->posx = 0; this->posy++; // check range/"scroll" } return tc; } } void Terminal::putstring(std::string text) { // This gets noisy when called from render effect ^D // ZF_LOGI("console_char %lu chars", chars.size()); for (auto strit = text.begin(); strit != text.end(); strit++) putchar(*strit); } // Old non-C++ ways void console_init(struct console_details *cdp) { cdp->posx = 0; cdp->posy = 0; cdp->savedx = 0; cdp->savedy = 0; cdp->in_ansi = 0; cdp->fgcolor = 7; cdp->bgcolor = 0; cdp->status = 0; } /* * Given the ansi number codes * Figure out fg, bg and status. */ void ansi_color(struct console_details *cdp, int color) { // There are many calls to this. // ZF_LOGV("ansi_color(%d)", color); if (color == 0) { cdp->status = 0; cdp->fgcolor = 7; cdp->bgcolor = 0; return; } if ((color == 1) || (color == 2) || (color == 3) || (color == 4) || (color == 5)) { cdp->status = color; return; } if ((color >= 30) && (color <= 37)) { cdp->fgcolor = color - 30; return; } if ((color >= 40) && (color <= 47)) { cdp->bgcolor = color - 40; return; } if (color == 39) { // default fg color cdp->fgcolor = 7; return; } if (color == 49) { // default bg color cdp->bgcolor = 0; return; } ZF_LOGD("ansi_color( %d ) is unknown to me.", color); } const char *color_restore(struct console_details *cdp) { static char buffer[30]; int slen; // possible optimize: If fg is 7, don't set (0 reset does that), if bg is 0, // don't set (0 does that) if (cdp->status == 0) { slen = snprintf(buffer, sizeof(buffer), "\x1b[0;3%d;4%dm", cdp->fgcolor, cdp->bgcolor); if (slen >= (int)sizeof(buffer)) { ZF_LOGE("snprintf %d > size %d", slen, (int)sizeof(buffer)); buffer[0] = 0; } } else { slen = snprintf(buffer, sizeof(buffer), "\x1b[0;%d;3%d;4%dm", cdp->status, cdp->fgcolor, cdp->bgcolor); if (slen >= (int)sizeof(buffer)) { ZF_LOGE("snprintf %d > size %d", slen, (int)sizeof(buffer)); buffer[0] = 0; } }; return buffer; } /* store cursor position X,Y */ std::vector> cursor_position_history; ANSI_TYPE console_ansi(struct console_details *cdp, const char *ansi) { const char *cp = ansi; const char *last = ansi + strlen(ansi) - 1; int number, number2; if (*cp == '\x1b') { cp++; // Ok, that's expected. if (*cp == '[') { cp++; // https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences switch (*last) { case 'A': // cursor up if (cp == last) { number = 1; } else { number = atoi(cp); if (number < 1) { ZF_LOGD("console_ansi( %s ): number error (%d)", repr(ansi), number); number = 1; } }; cdp->posy -= number; if (cdp->posy < 0) { cdp->posy = 0; ZF_LOGD( "console_ansi( %s ): attempt to move above top of screen (%d)", repr(ansi), number); } return CURSOR; case 'B': // cursor down if (cp == last) { number = 1; } else { number = atoi(cp); if (number < 1) { ZF_LOGD("console_ansi( %s ): number error (%d)", repr(ansi), number); number = 1; } }; cdp->posy += number; // check range/"scroll" return CURSOR; case 'C': // cursor forward if (cp == last) { number = 1; } else { number = atoi(cp); if (number < 1) { ZF_LOGD("console_ansi( %s ): number error (%d)", repr(ansi), number); number = 1; } }; cdp->posx += number; // Well. According to the "spec", the screen limits are hard // If the cursor is already at the edge of the screen, this has no // effect. ^ This is *NOT* how any ANSI BBS terminal program acts! while (cdp->posx > 79) { cdp->posy++; // check range/"scroll" cdp->posx -= 79; } return CURSOR; case 'D': // cursor backwards if (cp == last) { number = 1; } else { number = atoi(cp); if (number < 1) { ZF_LOGD("console_ansi( %s ): number error (%d)", repr(ansi), number); number = 0; } }; cdp->posx -= number; // Well. According to the "spec", the screen limits are hard // If the cursor is already at the edge of the screen, this has no // effect. ^ This is *NOT* how any ANSI BBS terminal program acts! while (cdp->posx < 0) { cdp->posy--; if (cdp->posy < 0) { cdp->posy = 0; } cdp->posx += 79; } return CURSOR; case 'H': // cursor position if (*cp == ';') { // Missing first number number = 1; cp++; if (cp == last) { // missing 2nd number as well? number2 = 1; } else { number2 = atoi(cp); } } else { // Ok, find the first number number = atoi(cp); cp = strchr(cp, ';'); if (cp == NULL) { // Missing 2nd number number2 = 1; } else { // 2nd number found, maybe. cp++; if (cp == last) { number2 = 1; } else { number2 = atoi(cp); } } } // Our positions start at zero, not one. cdp->posy = number - 1; cdp->posx = number2 - 1; return CURSOR; case 'J': // clear if (cp == last) { number = 0; } else { number = atoi(cp); }; // clears ... part of the screen. if (number == 2) { cdp->posx = 0; cdp->posy = 0; }; return CLEAR; case 'K': // clear line if (cp == last) { number = 0; } else { number = atoi(cp); }; return CLEAR; case 'm': // color if (cp == last) { // nothing given, default to 0. number = 0; ansi_color(cdp, number); } else { while (cp != last) { number = atoi(cp); ansi_color(cdp, number); cp++; while ((cp != last) && (isdigit(*cp))) { cp++; }; if (cp != last) { if (*cp == ';') { cp++; } } } } return COLOR; case 's': // save position cursor_position_history.push_back(std::make_pair(cdp->posx, cdp->posy)); return CURSOR; case 'u': // restore position if (cursor_position_history.empty()) { ZF_LOGE("Restore cursor position from empty history."); } else { std::pair pos = cursor_position_history.back(); cdp->posx = pos.first; cdp->posy = pos.second; cursor_position_history.pop_back(); } return CURSOR; case 't': case 'r': case 'h': case '!': // These are ones that I don't care about. case 'n': // This is terminal detect -- give me cursor position return OTHER; default: // unsure -- possibly not important ZF_LOGD("console_ansi( %s ): ???", repr(ansi)); return OTHER; } } } ZF_LOGD("console_ansi( %s ) : ???", repr(ansi)); return OTHER; } /* * console_char() * return whether or not we are still in_ansi * * in_ansi TRUE: START (receiving start of/fragment of ANSI) * CURSOR, COLOR, CLEAR, OTHER * Results of the last ANSI parse. * in_ansi FALSE: START Normal character. * */ termchar console_char(struct console_details *cdp, char ch) { char *cp; termchar tc; if (cdp->in_ansi) { // Ok, append this char cp = cdp->ansi + strlen(cdp->ansi); *cp = ch; cp++; *cp = 0; if (isalpha(ch)) { // Ok! end of ANSI code, process it. tc.ansi = console_ansi(cdp, cdp->ansi); cdp->in_ansi = 0; cdp->ansi[0] = 0; tc.in_ansi = 1; return tc; } tc.ansi = START; tc.in_ansi = 1; return tc; } else { tc.ansi = START; tc.in_ansi = 0; if (ch == '\x1b') { cp = cdp->ansi; *cp = ch; cp++; *cp = 0; cdp->in_ansi = 1; tc.in_ansi = 1; return tc; } // should I try reporting MOTION non-ANSI ? if (ch == '\r') { // Carriage return cdp->posx = 0; return tc; } if (ch == '\n') { cdp->posy++; // check range/"scroll" return tc; } if (ch == '\b') { // Backspace. if (cdp->posx > 0) { cdp->posx--; } return tc; } if (ch == '\f') { // form feed // treat as clear screen cdp->posx = 0; cdp->posy = 0; return tc; } /* I don't believe that anything else can possibly be here, other then an actual printable character. So! FUTURE: Store the screen text + colors */ cdp->posx++; if (cdp->posx > 79) { cdp->posx = 0; cdp->posy++; // check range/"scroll" } return tc; } } void console_receive(struct console_details *cdp, std::string chars) { // This gets noisy when called from render effect ^D // ZF_LOGI("console_char %lu chars", chars.size()); for (auto strit = chars.begin(); strit != chars.end(); strit++) console_char(cdp, *strit); }