diff --git a/src/Home/Net/Drivers/PCNet.ZC b/src/Home/Net/Drivers/PCNet.ZC index 637956de8..a3fb20114 100755 --- a/src/Home/Net/Drivers/PCNet.ZC +++ b/src/Home/Net/Drivers/PCNet.ZC @@ -79,6 +79,7 @@ #define PCNET_DESCRIPTORf_ENP 24 #define PCNET_DESCRIPTORf_STP 25 +#define PCNET_DESCRIPTORf_ERR 30 // set when FRAM/OFLO/CRC/BUFF error occurred on this frame #define PCNET_DESCRIPTORf_OWN 31 // AMD PCNet datasheet p.1-992, 1-994 class CPCNet @@ -148,7 +149,7 @@ U0 PCNetReset() InU32(PCNetIOBaseGet + PCNET_DW_RESET); InU16(PCNetIOBaseGet + PCNET_WD_RESET); - Busy(5); // OSDev says minimum 1 µS + Busy(5); // OSDev says minimum 1 ┬╡S } U0 PCNet32BitModeEnable() @@ -214,22 +215,22 @@ U32 PCNetCSRRead(U32 csr) } U0 PCNetAutoLinkSelect() -{ - U32 bcr = PCNetCSRRead(PCNET_BCR_FULL_DUPLEX_CTRL); +{ // BCR2 ASEL: let the card auto-select the media interface. + U32 bcr = PCNetBCRRead(PCNET_BCR_MISC_CONFIG); - Bts(&bcr, PCNET_BCR_FULL_DUPLEX_CTRL_FDEN); - Bts(&bcr, PCNET_BCR_FULL_DUPLEX_CTRL_AUIFD); + Bts(&bcr, PCNET_BCR_MISC_CONFIG_ASEL); - PCNetBCRWrite(PCNET_BCR_FULL_DUPLEX_CTRL, bcr); + PCNetBCRWrite(PCNET_BCR_MISC_CONFIG, bcr); } U0 PCNetEnableFullDuplex() -{ - U32 bcr = PCNetCSRRead(PCNET_BCR_MISC_CONFIG); +{ // BCR9 FDEN/AUIFD: enable full-duplex operation. + U32 bcr = PCNetBCRRead(PCNET_BCR_FULL_DUPLEX_CTRL); - Bts(&bcr, PCNET_BCR_MISC_CONFIG_ASEL); + Bts(&bcr, PCNET_BCR_FULL_DUPLEX_CTRL_FDEN); + Bts(&bcr, PCNET_BCR_FULL_DUPLEX_CTRL_AUIFD); - PCNetBCRWrite(PCNET_BCR_MISC_CONFIG, bcr); + PCNetBCRWrite(PCNET_BCR_FULL_DUPLEX_CTRL, bcr); } @@ -676,15 +677,24 @@ interrupt U0 PCNetIRQ() NetLog("$$BD,CYAN$$$$FD,WHITE$$" "PCNET IRQ: Saw owned RX DE index %d.", pcnet.current_rx_de_index); - de_index = PCNetPacketReceive(&packet_buffer, &packet_length); - - if (de_index >= 0) // todo: necessary? check increment logic in PCNetPacketReceive. - { - NetLog("PCNET IRQ: Pushing copy into Net Queue, releasing receive packet."); - // uncached read - NetQueuePush(packet_buffer + dev.uncached_alias, packet_length); + if (PCIBt(&entry[pcnet.current_rx_de_index].status1, PCNET_DESCRIPTORf_ERR)) + { // drop frames the card flagged with FRAM/OFLO/CRC/BUFF errors + NetErr("PCNET IRQ: RX DE %d has error status, dropping frame.", pcnet.current_rx_de_index); + de_index = PCNetPacketReceive(&packet_buffer, &packet_length); PCNetReceivePacketRelease(de_index); } + else + { + de_index = PCNetPacketReceive(&packet_buffer, &packet_length); + + if (de_index >= 0) // todo: necessary? check increment logic in PCNetPacketReceive. + { + NetLog("PCNET IRQ: Pushing copy into Net Queue, releasing receive packet."); + // uncached read + NetQueuePush(packet_buffer + dev.uncached_alias, packet_length); + PCNetReceivePacketRelease(de_index); + } + } Bts(&csr, PCNET_CTRL_RINT); diff --git a/src/Home/Net/Load.ZC b/src/Home/Net/Load.ZC index 1dc0981e9..ca25d3e34 100755 --- a/src/Home/Net/Load.ZC +++ b/src/Home/Net/Load.ZC @@ -22,6 +22,8 @@ Cd(__DIR__);; #include "Protocols/TCP/MakeTCP" +#include "Protocols/HTTP" // needs DNS + TCP + #include "Protocols/DHCP" #include "Utilities/NetHandler" // needs IPV4, UDP, ICMP \ No newline at end of file diff --git a/src/Home/Net/Programs/Browser.ZC b/src/Home/Net/Programs/Browser.ZC new file mode 100644 index 000000000..2fb542145 --- /dev/null +++ b/src/Home/Net/Programs/Browser.ZC @@ -0,0 +1,361 @@ +// Zeb: a minimal text-mode web browser for ZealOS. Public Domain. +// Fetches pages with the HTTP client (Protocols/HTTP.ZC), strips HTML to +// plain text, numbers the links, and lets you follow them by number. +// Plain HTTP only for now: HTTPS needs a TLS stack ZealOS doesn't have yet. + +#define ZEB_MAX_LINKS 256 +#define ZEB_HISTORY_MAX 64 + +U8 *zeb_links[ZEB_MAX_LINKS]; +I64 zeb_link_count = 0; + +U0 ZebLinksFree() +{ + I64 i; + + for (i = 0; i < zeb_link_count; i++) + Free(zeb_links[i]); + + zeb_link_count = 0; +} + +U0 ZebPutS(U8 *s) +{ // print untrusted text: double dollar signs so DolDoc takes them literally + while (*s) + { + if (*s == '$$') + "$$$$"; + else + "%c", *s; + s++; + } +} + +Bool ZebTagIsBreak(U8 *tag) +{ // tags that force a line break in the text rendering + return !StrICompare(tag, "br") || !StrICompare(tag, "p") || + !StrICompare(tag, "div") || !StrICompare(tag, "tr") || + !StrICompare(tag, "li") || !StrICompare(tag, "ul") || + !StrICompare(tag, "ol") || !StrICompare(tag, "table")|| + !StrICompare(tag, "h1") || !StrICompare(tag, "h2") || + !StrICompare(tag, "h3") || !StrICompare(tag, "h4") || + !StrICompare(tag, "h5") || !StrICompare(tag, "h6") || + !StrICompare(tag, "hr") || !StrICompare(tag, "blockquote"); +} + +U8 *ZebHtmlToText(U8 *html) +{ // Strip tags/entities, collect targets into zeb_links, append a + // [n] marker after each link's anchor text. Returns MAlloc'ed text. + // Output worst case: every byte a dollar doubled, plus "[nnn]" per link. + I64 len = StrLen(html); + U8 *out = MAlloc(2 * len + 8 * ZEB_MAX_LINKS + 16); + I64 o = 0; + U8 *p = html; + U8 tag[16]; + I64 tag_len; + Bool closing; + U8 *attrs, *attrs_end, *v; + U8 href[1024]; + I64 href_len; + Bool link_open = FALSE; + U8 *marker; + I64 entity; + U8 c, last_out = '\n'; + I64 newline_run = 1; + + ZebLinksFree; + + while (*p) + { + if (*p == '<') + { + p++; + closing = (*p == '/'); + if (closing) + p++; + + tag_len = 0; + while (*p && *p != '>' && *p != ' ' && *p != '\t' && + *p != '\r' && *p != '\n' && tag_len < 15) + tag[tag_len++] = *p++; + tag[tag_len] = 0; + + attrs = p; // attribute region runs to '>' + while (*p && *p != '>') + p++; + attrs_end = p; + if (*p) + p++; + + if (!closing && !StrICompare(tag, "a")) + { // find href= inside the attribute region + href_len = 0; + v = attrs; + while (v + 5 < attrs_end && StrNICompare(v, "href", 4)) + v++; + if (v + 5 < attrs_end) + { + v += 4; + while (v < attrs_end && (*v == ' ' || *v == '\t' || *v == '=')) + v++; + if (v < attrs_end && (*v == '"' || *v == '\'')) + { + c = *v++; + while (v < attrs_end && *v != c && href_len < 1023) + href[href_len++] = *v++; + } + else + { + while (v < attrs_end && *v != ' ' && *v != '\t' && + *v != '>' && href_len < 1023) + href[href_len++] = *v++; + } + } + href[href_len] = 0; + + if (href_len && zeb_link_count < ZEB_MAX_LINKS) + { + zeb_links[zeb_link_count++] = StrNew(href); + link_open = TRUE; + } + } + else if (closing && !StrICompare(tag, "a")) + { + if (link_open) + { // append "[n]" marker after the anchor text + marker = MStrPrint("[%d]", zeb_link_count); + MemCopy(out + o, marker, StrLen(marker)); + o += StrLen(marker); + Free(marker); + last_out = ']'; + newline_run = 0; + link_open = FALSE; + } + } + else if (!closing && (!StrICompare(tag, "script") || !StrICompare(tag, "style"))) + { // skip embedded script/style content up to the matching close tag + while (*p) + { + if (*p == '<' && p[1] == '/' && !StrNICompare(p + 2, tag, tag_len)) + { + while (*p && *p != '>') + p++; + if (*p) + p++; + break; + } + p++; + } + } + + if (ZebTagIsBreak(tag) && newline_run < 2) + { + out[o++] = '\n'; + last_out = '\n'; + newline_run++; + } + } + else if (*p == '&') + { // character entity + if (!StrNICompare(p, "&", 5)) { c = '&'; p += 5; } + else if (!StrNICompare(p, "<", 4)) { c = '<'; p += 4; } + else if (!StrNICompare(p, ">", 4)) { c = '>'; p += 4; } + else if (!StrNICompare(p, """, 6)) { c = '"'; p += 6; } + else if (!StrNICompare(p, "'", 6)) { c = '\''; p += 6; } + else if (!StrNICompare(p, " ", 6)) { c = ' '; p += 6; } + else if (p[1] == '#') + { + entity = Str2I64(p + 2); + c = entity & 0x7F; + if (c < 32) + c = '?'; + while (*p && *p != ';') + p++; + if (*p) + p++; + } + else + { + c = '&'; + p++; + } + + if (c == '$$') + { + out[o++] = '$$'; + out[o++] = '$$'; + } + else + out[o++] = c; + last_out = c; + newline_run = 0; + } + else + { + c = *p++; + + if (c == '\r') + c = 0; // dropped + else if (c == '\n' || c == '\t') + c = ' '; + + if (c >= 0x80) + { // UTF-8: '?' for lead bytes, drop continuation bytes + if (c >= 0xC0) + c = '?'; + else + c = 0; + } + + if (c == ' ' && (last_out == ' ' || last_out == '\n')) + c = 0; // collapse whitespace runs + + if (c == '$$') + { + out[o++] = '$$'; + out[o++] = '$$'; + last_out = '$$'; + newline_run = 0; + } + else if (c) + { + out[o++] = c; + last_out = c; + newline_run = 0; + } + } + } + + out[o] = 0; + return out; +} + +U0 ZebShowPage(U8 *url, CHttpResponse *response) +{ + U8 *text = ZebHtmlToText(response->body); + + "\n$$LTBLUE$$--- "; + ZebPutS(url); + " ---$$FG$$\n"; + + if (response->status != 200) + "$$YELLOW$$HTTP status: %d$$FG$$\n", response->status; + + "\n"; + "%s", text; + "\n$$LTBLUE$$--- end (%d links) ---$$FG$$\n", zeb_link_count; + + Free(text); +} + +public U0 Zeb(U8 *url=NULL) +{ + U8 *history[ZEB_HISTORY_MAX]; + I64 hist_count = 0; + U8 *current, *input, *target; + I64 n; + CHttpUrl parsed; + CHttpResponse *response; + Bool running = TRUE, fetch_needed = TRUE; + + if (url) + current = StrNew(url); + else + { + current = StrGet("URL:"); + if (!current || !*current) + { + Free(current); + return; + } + } + + while (running) + { + if (fetch_needed) + { + "$$YELLOW$$Fetching "; + ZebPutS(current); + " ...$$FG$$\n"; + + response = HttpGet(current); + if (response) + { + ZebShowPage(current, response); + HttpResponseFree(response); + } + else + "$$RED$$Fetch failed. See Network Log. (Note: HTTPS is not supported yet.)$$FG$$\n"; + + fetch_needed = FALSE; + } + + input = StrGet("Zeb [#=follow link, u=new URL, b=back, r=reload, q=quit]:"); + + if (!input || !StrCompare(input, "q")) + running = FALSE; + else if (!StrCompare(input, "r")) + fetch_needed = TRUE; + else if (!StrCompare(input, "b")) + { + if (hist_count) + { + Free(current); + current = history[--hist_count]; + fetch_needed = TRUE; + } + else + "No history.\n"; + } + else if (!StrCompare(input, "u")) + { + target = StrGet("URL:"); + if (target && *target) + { + if (hist_count < ZEB_HISTORY_MAX) + history[hist_count++] = current; + else + Free(current); + current = target; + fetch_needed = TRUE; + } + else + Free(target); + } + else + { + n = Str2I64(input); + if (n >= 1 && n <= zeb_link_count) + { + if (HttpUrlParse(current, &parsed) < 0) + "Bad current URL.\n"; + else + { + target = HttpUrlResolve(&parsed, zeb_links[n - 1]); + if (target) + { + if (hist_count < ZEB_HISTORY_MAX) + history[hist_count++] = current; + else + Free(current); + current = target; + fetch_needed = TRUE; + } + else + "Unsupported link scheme (mailto/javascript/anchor).\n"; + } + } + else + "No such link. Links are 1-%d.\n", zeb_link_count; + } + + Free(input); + } + + Free(current); + while (hist_count) + Free(history[--hist_count]); + ZebLinksFree; +} + +Zeb; diff --git a/src/Home/Net/Programs/Telnet/Telnet.ZC b/src/Home/Net/Programs/Telnet/Telnet.ZC index 8b7d7a537..ff8ce88cf 100755 --- a/src/Home/Net/Programs/Telnet/Telnet.ZC +++ b/src/Home/Net/Programs/Telnet/Telnet.ZC @@ -4,8 +4,7 @@ Cd(__DIR__);; #define TELNET_PORT 23 -// #define BUF_SIZE 8192 // way too big? -#define BUF_SIZE 819200 // way too big? +#define BUF_SIZE 819200 #define INPUT_BUF_SIZE 32 #define TIMEOUT_DURATION 500000 @@ -18,202 +17,349 @@ Cd(__DIR__);; #include "TelnetClass" #include "TelnetHelpers" +#include "TelnetNegotiation" #include "TelnetANSI" // If you're using a custom palette, the colors might not seem right. CBGR24 original_palette[COLORS_NUM]; -Bool dark_mode = TRUE; // since ZealOS is dark by default Bool original_colors = FALSE; Bool force_disconnect = FALSE; +U0 HandleControlCodes(U8 ch) { + switch (ch) + { + case 0: // NUL - ignored + break; + case 7: // BEL + Beep; + break; + case 8: // BS + if (term.csr_col) + term.csr_col--; + break; + case 9: // HT: next 8-column tab stop + term.csr_col = (term.csr_col & ~7) + 8; + if (term.csr_col >= term.window_width) + term.csr_col = term.window_width - 1; + break; + case 10: // LF + case 11: // VT behaves like LF + TermLineFeed; + break; + case 12: // FF + TermClear; + break; + case 13: // CR + term.csr_col = 0; + break; + default: + if (ch >= 32) // printable + CP437 high glyphs + TermPutChar(ch); + // remaining C0 control codes: ignored + break; + } +} + +U0 TelnetByteProcess(U8 ch) +{ // Stream state machine: one byte at a time, state persists in term across + // TCPSocketReceive calls, so telnet commands and ANSI escape sequences split + // over TCP segment boundaries are parsed correctly instead of printed as garbage. + switch (term.parse_state) { + case TELNET_STATE_NORMAL: + if (ch == NEGOTIATE) + term.parse_state = TELNET_STATE_IAC; + else if (ch == ANSI_ESC) + term.parse_state = TELNET_STATE_ESC; + else + HandleControlCodes(ch); + break; + + case TELNET_STATE_IAC: + switch (ch) { + case NEGOTIATE: // IAC IAC is an escaped literal 0xFF data byte + HandleControlCodes(NEGOTIATE); + term.parse_state = TELNET_STATE_NORMAL; + break; + case WILL: + case WONT: + case DO: + case DONT: + term.iac_cmd = ch; + term.parse_state = TELNET_STATE_IAC_OPT; + break; + case SB: + term.sb_len = 0; + term.parse_state = TELNET_STATE_SB; + break; + default: // NOP, GA, AYT, ... : consume and ignore + term.parse_state = TELNET_STATE_NORMAL; + break; + } + break; + + case TELNET_STATE_IAC_OPT: + if (term.sock_ready) + TelnetNegotiateOption(term.sock, term.iac_cmd, ch); + term.parse_state = TELNET_STATE_NORMAL; + break; + + case TELNET_STATE_SB: + if (ch == NEGOTIATE) + term.parse_state = TELNET_STATE_SB_IAC; + else if (term.sb_len < TELNET_SB_BUF_SIZE) + term.sb_buf[term.sb_len++] = ch; + break; + + case TELNET_STATE_SB_IAC: + if (ch == SE) { // IAC SE terminates the subnegotiation + if (term.sock_ready) + TelnetSubnegotiate(term.sock, term.sb_buf, term.sb_len); + term.parse_state = TELNET_STATE_NORMAL; + } + else if (ch == NEGOTIATE) { // IAC IAC inside SB: literal 0xFF payload byte + if (term.sb_len < TELNET_SB_BUF_SIZE) + term.sb_buf[term.sb_len++] = NEGOTIATE; + term.parse_state = TELNET_STATE_SB; + } + else // malformed subnegotiation, bail out + term.parse_state = TELNET_STATE_NORMAL; + break; + + case TELNET_STATE_ESC: + switch (ch) + { + case ANSI_CSI: + term.seq_len = 0; + term.parse_state = TELNET_STATE_CSI; + break; + case '(': // charset designation: swallow the byte that follows + case ')': + term.parse_state = TELNET_STATE_ESC_CHARSET; + break; + case '7': // save cursor + term.saved_row = term.csr_row; + term.saved_col = term.csr_col; + term.parse_state = TELNET_STATE_NORMAL; + break; + case '8': // restore cursor + TermCursorSet(term.saved_row, term.saved_col); + term.parse_state = TELNET_STATE_NORMAL; + break; + case 'M': // reverse index: up, scroll down at top + if (term.csr_row) + term.csr_row--; + else + TermScrollDown; + term.parse_state = TELNET_STATE_NORMAL; + break; + case 'D': // index: down + TermLineFeed; + term.parse_state = TELNET_STATE_NORMAL; + break; + case 'E': // next line + term.csr_col = 0; + TermLineFeed; + term.parse_state = TELNET_STATE_NORMAL; + break; + case 'c': // full reset + term.cur_attr = TERM_DEFAULT_ATTR; + term.bold = FALSE; + TermClear; + term.parse_state = TELNET_STATE_NORMAL; + break; + default: // unknown ESC x: swallow + term.parse_state = TELNET_STATE_NORMAL; + break; + } + break; + + case TELNET_STATE_ESC_CHARSET: + term.parse_state = TELNET_STATE_NORMAL; // designation byte consumed + break; + + case TELNET_STATE_CSI: + if (IsAlpha(ch) || term.seq_len >= TELNET_SEQ_BUF_SIZE - 2) { + term.seq_buf[term.seq_len++] = ch; // final byte (or overlong sequence force-cut) + term.seq_buf[term.seq_len] = '\0'; + HandleANSISequence(term.seq_buf); + term.parse_state = TELNET_STATE_NORMAL; + } + else + term.seq_buf[term.seq_len++] = ch; + break; + } +} + +U0 ANSIParse() +{ + I64 i; + + term.view_offset = 0; // new output snaps the view back to live + + for (i = 0; i < term.buffer_len; i++) + TelnetByteProcess(term.buffer[i]); +} + +U0 TermPutS(U8 *s) +{ // Local status text, fed through the same parser so it can use ANSI colors. + term.parse_state = TELNET_STATE_NORMAL; // never inherit a half-parsed server sequence + while (*s) + TelnetByteProcess(*s++); +} + I64 TelnetOpen(U8 *host, U16 port) { I64 sock; + U8 *msg; if (host == NULL) { return -1; } - DocPrint(term.doc, "$$GREEN$$Connecting to %s:%d.$$FG$$$$BG$$\n", host, port); + + msg = MStrPrint("\x1B[32mConnecting to %s:%d.\x1B[0m\r\n", host, port); + TermPutS(msg); + Free(msg); + sock = TCPConnectionCreate(host, port); if (sock <= 0) { - // PrintErr("Failed to connect to %s:%d\n", host, port); PopUpOk("\n\n\tFailed to connect\n\n"); return sock; } - // sock(CTCPSocket *)->timeout = 0; - // sock(CTCPSocket *)->timeout = TCP_TIMEOUT; - return sock; + return sock; // caller sets the socket timeout for poll-mode receive } -U0 TermBottom() -{ - Bool unlock; - CDoc *doc = term.doc; - - unlock = DocLock(doc); - doc->cur_entry = doc; - doc->cur_col = 0; - if (unlock) - DocUnlock(doc); -} +U0 TerminalTask() { + I64 received; -U0 HandleControlCodes(U8 ch) { - if (ch < 32) { // ASCII code below 32 (control character) - switch (ch) { - case 0: // NUL (Null) - Typically ignored - break; - case 7: // BEL (Bell) - Beep; - break; - case 8: // BS (Backspace) - DocPrint(term.doc, "$$CM,-1,0$$"); - break; - case 9: // HT (Horizontal Tab) - DocPrint(term.doc, "$$CM,8,0$$"); - break; - case 10: // LF (Line Feed) - // DocPrint(term.doc, "\n"); - break; - case 11: // VT (Vertical Tab) - SysLog("Vertical Tab\n"); - break; - case 12: // FF (Form Feed) - // SysLog("form feed\n"); - DocClear(term.doc); - // DocPrint(term.doc, "\f"); - break; - case 13: // CR (Carriage Return) - // DocPrint(term.doc, "\r"); - DocPrint(term.doc, "$$CM+LX+PRY,LE=0,RE=1$$"); - break; - case 14: // SO (Shift Out) - Switch to an alternate character set - case 15: // SI (Shift In) - Switch back to the default character set - SysLog("Shift In/Out\n"); - break; - case 22: - SysLog("Synchronous Idle\n"); - break; - case 23: - SysLog("End of Transmission Block\n"); - break; - case 24: - SysLog("Cancel\n"); - break; - case 25: - SysLog("End of Medium\n"); - break; - case 26: - SysLog("Sub\n"); - break; - case 27: - SysLog("Esc\n"); - break; - case 28: - SysLog("Fs\n"); - break; - case 29: - SysLog("Gs\n"); - break; - case 30: - SysLog("Rs\n"); - break; - case 31: - SysLog("Unit Separator\n"); - break; - default: - // some ch make Zeal crash or behave weird because they're commands? - // SysLog("CC %c happened\n", ch); - SysLog("CC 0x%X happened\n", ch); - break; - } - } - else { - if (ch == 127) { - SysLog("case 127"); - } - if (ch == 0x24) { - DocPrint(term.doc, "$$$$"); - } - if (ch >= 32 && ch < 256) // ZealOS's ASCII is up to 255 - { - DocPrint(term.doc, "%c", ch); + while (!force_disconnect) { + // Claim busy BEFORE checking sock_ready: TelnetDisconnect clears sock_ready + // then waits on task_idle, so this order means it can never observe a stale + // idle flag while we are about to touch the socket. + term.task_idle = FALSE; + + if (!term.sock_ready) { + term.task_idle = TRUE; // main task may now safely close/free the socket + Sleep(50); } else { - DocPrint(term.doc, "?"); // unrecognized character + received = TCPSocketReceive(term.sock, term.buffer, BUF_SIZE - 1); + + if (received > 0) { + term.buffer_len = received; + ANSIParse; + } + else if (received == 0) { // orderly close or reset by remote + term.sock_ready = 0; + TermPutS("\r\n\x1B[1;37;41mConnection closed by remote host. Shift-Esc for splash.\x1B[0m\r\n"); + } + else + Sleep(20); // non-blocking receive: nothing pending yet } } + + term.task_idle = TRUE; } -U0 ANSIParse() -{ - // Basic Telnet protocol parser - U8 *ptr = term.buffer; - while (ptr < term.buffer + term.buffer_len) { - // disable all SAUCE00 art signature? dsnt work - // if (StrNCompare(ptr, "\033SAUCE", 6) == 0) - // { - // SysLog("SAUCE found\n"); - // term.buffer_len = ptr - term.buffer; - // } - // Telnet negotiation sequence - if (*ptr == NEGOTIATE) { - /*telnet negotiation seems proper...however i don't really see any BBS systems relying on this too much... - for instance, the screen size tends to be reported using the Curser Report and not Telnet's NAWS */ - if (term.sock_ready) TelnetNegotiate(term.sock, ptr); - ptr += 3; - } - else if (*ptr == ANSI_ESC) { - // ANSI escape sequence - ptr++; - if (*ptr == ANSI_CSI) { +U0 TelnetDisconnect() +{ // Close the current socket without racing TerminalTask's receive path. + if (term.sock == 0) + return; + + term.sock_ready = 0; - ptr++; // Move past '[' + while (!term.task_idle) + Sleep(10); // wait for TerminalTask to finish its in-flight receive - U8 seqBuffer[64]; - MemSet(seqBuffer, 0, sizeof(seqBuffer)); // Set all bytes in seqBuffer to 0 + term.sock(CTCPSocket *)->timeout = 500; // bounded graceful FIN handshake + TCPSocketClose(term.sock); + term.sock = 0; +} - U8 *seqPtr = seqBuffer; +U8 TermPaletteColor(U8 c, Bool invert) +{ // The grid stores canonical ANSI/VGA indices (0 = black, 15 = white). + // ZealOS dark themes swap palette entries 0 and 15, so translate at draw + // time; the middle colors are theme-tuned but keep their hue. + if (invert) + { + if (c == BLACK) + return WHITE; + if (c == WHITE) + return BLACK; + } + return c; +} - while (*ptr && !IsAlpha(*ptr) && (seqPtr - seqBuffer < sizeof(seqBuffer) - 1)) { - *seqPtr++ = *ptr++; - } - *seqPtr++ = *ptr++; // Append the final letter - *seqPtr = '\0'; // Null-terminate +U0 TelnetDrawIt(CTask *task, CDC *dc) +{ // Blit the visible rows (scrollback + live grid), one background rect per + // same-attribute run, then glyphs. + I64 r, c, i, run_start, csr_col, view_start, idx, ring; + U8 attr, ch; + U8 *row_ch, *row_attr; + Bool invert; - HandleANSISequence(seqBuffer); - } + no_warn task; + + // Detect swapped-endpoint palettes (ZealOS dark theme) each frame, so + // Alt+T standard-palette toggling and theme changes stay correct. + invert = gr_palette[BLACK].b + gr_palette[BLACK].g + gr_palette[BLACK].r > + gr_palette[WHITE].b + gr_palette[WHITE].g + gr_palette[WHITE].r; + + // Visible window starts view_offset rows back into the combined + // [history ring][live screen] row sequence. + view_start = term.back_count - term.view_offset; + + for (r = 0; r < term.window_height; r++) + { + idx = view_start + r; + if (idx < term.back_count) + { // history row: translate combined index to ring slot + ring = (term.back_head - term.back_count + idx + TERM_SCROLLBACK_ROWS) % TERM_SCROLLBACK_ROWS; + row_ch = term.back_ch + ring * TERM_MAX_COLS; + row_attr = term.back_attr + ring * TERM_MAX_COLS; } - else { - // Print the received character - HandleControlCodes(*ptr); - ptr++; + else + { // live screen row + row_ch = term.screen_ch + (idx - term.back_count) * TERM_MAX_COLS; + row_attr = term.screen_attr + (idx - term.back_count) * TERM_MAX_COLS; } - } -} -U0 TerminalTask() { - while (!term.sock_ready) { - Sleep(100); // Avoid busy waiting - } + c = 0; + while (c < term.window_width) + { + run_start = c; + attr = row_attr[c]; + while (c < term.window_width && row_attr[c] == attr) + c++; + + dc->color = TermPaletteColor(attr >> 4, invert); // background + GrRect(dc, run_start * FONT_WIDTH, r * FONT_HEIGHT, + (c - run_start) * FONT_WIDTH, FONT_HEIGHT); - while (term.sock_ready && !force_disconnect) { -receive_data: - term.buffer_len = TCPSocketReceive(term.sock, term.buffer, BUF_SIZE - 1); - if (term.buffer_len > 0) { - term.buffer[term.buffer_len] = '\0'; - // parse the buffer - ANSIParse; - } else { - //SysLog("BUF_SIZE: %d\n", BUF_SIZE); - if (!term.sock_ready || force_disconnect) - DocPrint(term.doc, "Error: Connection closed by the remote host.\n"); - else { - //SysLog("goto received_data\n"); - goto receive_data; + dc->color = TermPaletteColor(attr & 0x0F, invert); // foreground glyphs + for (i = run_start; i < c; i++) + { + ch = row_ch[i]; + if (ch != ' ') + GrPutChar(dc, i * FONT_WIDTH, r * FONT_HEIGHT, ch); } - break; } } + + if (term.view_offset) + { // scrolled into history: yellow marker bar along the top edge + dc->color = TermPaletteColor(YELLOW, invert); + GrRect(dc, 0, 0, term.window_width * FONT_WIDTH, 2); + } + else if (term.csr_visible && term.sock_ready) + { // underline-style cursor, live view only + csr_col = term.csr_col; + if (csr_col >= term.window_width) // deferred-wrap position parks past the edge + csr_col = term.window_width - 1; + + dc->color = TermPaletteColor(term.cur_attr & 0x0F, invert); + GrRect(dc, csr_col * FONT_WIDTH, + term.csr_row * FONT_HEIGHT + FONT_HEIGHT - 2, FONT_WIDTH, 2); + } } U0 Telnet(U8 *host=NULL, U16 port=TELNET_PORT) { @@ -221,15 +367,33 @@ U0 Telnet(U8 *host=NULL, U16 port=TELNET_PORT) { term.window_width = 80; term.window_height = 25; term.doc = Fs->display_doc; + term.win_task = Fs; term.waiting_for_input = TRUE; + term.sock = 0; term.sock_ready = 0; + term.task_idle = TRUE; + term.parse_state = TELNET_STATE_NORMAL; + term.buffer_len = 0; + term.cur_attr = TERM_DEFAULT_ATTR; + term.bold = FALSE; + term.csr_visible = TRUE; + term.csr_row = 0; + term.csr_col = 0; + term.saved_row = 0; + term.saved_col = 0; + force_disconnect = FALSE; // reset the global so a second run doesn't exit instantly I64 art_path = "::/Home/Net/Programs/Telnet/Art/TelnetSplash.ans"; - I64 message_code, arg1, arg2; GrPaletteGet(original_palette); AutoComplete(OFF); + TermClear; + + // Single receive task for the whole session; it idles while sock_ready is 0. + term.task = Spawn(&TerminalTask, NULL, "Telnet"); + DocMax; DocCursor(OFF); + DocClear; SettingsPush; MenuPush( @@ -241,8 +405,7 @@ U0 Telnet(U8 *host=NULL, U16 port=TELNET_PORT) { " ANSIArt(,SCF_ALT);" "}" "Config {" - " ToggleDarkMode(,SCF_ALT);" - " SetDarkColors(,SCF_ALT);" + " TogglePalette(,SCF_ALT);" "}" "About {" " Info(,SCF_ALT);" @@ -251,9 +414,6 @@ U0 Telnet(U8 *host=NULL, U16 port=TELNET_PORT) { StrCopy(Fs->task_title, "TELNET"); Fs->border_src = BDS_CONST; - // Fs->border_attr = LTGREEN << 4 + DriveTextAttrGet(':') & 15; - if (dark_mode) Fs->text_attr = WHITE << 4 + BLACK; - else Fs->text_attr = BLACK << 4 + WHITE; Fs->title_src = TTS_LOCKED_CONST; DocClear(Fs->border_doc, TRUE); @@ -268,53 +428,55 @@ U0 Telnet(U8 *host=NULL, U16 port=TELNET_PORT) { (Fs->win_height - 1), Fs); - // this flag will make the term auto-size to the content (ish) - // term.doc->flags |= DOCEG_DONT_EDIT; + // From here the window content is the cell grid, not the doc. + Fs->draw_it = &TelnetDrawIt; show_splash: - // SplashScreen Fs->border_attr = WHITE << 4 + LTRED; - DocClear; - // probably should use word wrap? dunno if it actually changes anything in this context... - DocPrint(, "$$WW,1$$"); - // Load the file into the buffer and get its size + term.cur_attr = TERM_DEFAULT_ATTR; + term.bold = FALSE; + TermScrollbackClear; + TermClear; + term.buffer_len = ANSIArtLoad(art_path, term.buffer); if (term.buffer_len > 0) { term.buffer[term.buffer_len] = '\0'; - // parse the buffer + term.parse_state = TELNET_STATE_NORMAL; // don't inherit half-parsed state from a dropped connection ANSIParse; - TermBottom; - // Free(term.buffer); } else { - DocClear; - Print("Error: Could not load splash screen.\n"); + TermClear; + TermPutS("Error: Could not load splash screen.\r\n"); } init_connection: while (!term.waiting_for_input || host != NULL) { - // if(term.sock != NULL) TCPSocketClose(term.sock); - // TODO: should probably kill the task if it already exists too? - - // Spawn a task to receive data from the socket - term.task = Spawn(&TerminalTask, NULL, "Telnet"); - + TelnetDisconnect; // safely drop any previous connection first term.sock = TelnetOpen(host, port); if (term.sock <= 0) { - // return; + term.sock = 0; term.waiting_for_input = TRUE; host = NULL; goto show_splash; } + + // Poll-mode receives: TerminalTask paces itself, and close paths never + // have to wait out a long blocking receive on a soon-to-be-freed socket. + term.sock(CTCPSocket *)->timeout = 0; + term.parse_state = TELNET_STATE_NORMAL; + Fs->border_attr = WHITE << 4 + GREEN; term.sock_ready = 1; // Signal that the socket is ready term.waiting_for_input = FALSE; - "$$BG,GREEN$$$$WHITE$$Connected$$FG$$$$BG$$\n"; - Sleep(1000); - DocClear; + TermPutS("\x1B[1;32mConnected\x1B[0m\r\n"); + Sleep(600); + term.cur_attr = TERM_DEFAULT_ATTR; + term.bold = FALSE; + TermScrollbackClear; + TermClear; break; } @@ -329,15 +491,10 @@ init_connection: { switch (key) { - // case 0: - // switch (sc.u8[0]) - // { - // } case 'n': TelnetPrompt(&form); host = form.host; port = form.port; - DocClear; term.waiting_for_input = FALSE; goto init_connection; break; @@ -346,23 +503,12 @@ init_connection: if (art_path != NULL) { term.sock_ready = 0; term.waiting_for_input = TRUE; - // SysLog("%s\n", art_path); goto show_splash; } else { - "Error: Could not load art.\n"; + TermPutS("Error: Could not load art.\r\n"); } break; - case 's': - // Save as DD image. - StrCopy(term.doc->filename.name, "::/Home/Wallpapers/1024/Default.DD"); - DocWrite(term.doc, TRUE); - break; - case 'd': - dark_mode = !dark_mode; - if (dark_mode) Fs->text_attr = WHITE << 4 + BLACK; - else Fs->text_attr = BLACK << 4 + WHITE; - break; case 't': original_colors = !original_colors; if (original_colors) PaletteSetStd(FALSE); @@ -372,7 +518,7 @@ init_connection: } break; case 'l': - PopUpOk("\n\n Not all BBS will work perfectly (yet). \n\n You can load ANSi artwork with Ctrl+O","\n\n\n\t\tMade by y4my4m\n\n"); + PopUpOk("\n\n Not all BBS will work perfectly (yet). \n\n You can load ANSi artwork with Alt+O","\n\n\n\t\tMade by y4my4m\n\n"); break; } } @@ -384,10 +530,22 @@ init_connection: { case SC_CURSOR_LEFT: if (term.sock_ready) TCPSocketSendString(term.sock, "\x1B[D"); break; case SC_CURSOR_RIGHT: if (term.sock_ready) TCPSocketSendString(term.sock, "\x1B[C"); break; - case SC_CURSOR_UP: if (term.sock_ready) TCPSocketSendString(term.sock, "\x1B[A"); break; - case SC_CURSOR_DOWN: if (term.sock_ready) TCPSocketSendString(term.sock, "\x1B[B"); break; - case SC_PAGE_UP: if (term.sock_ready) TCPSocketSendString(term.sock, "\033[V"); break; - case SC_PAGE_DOWN:if (term.sock_ready) TCPSocketSendString(term.sock, "\033[U"); break; + case SC_CURSOR_UP: // offline (art viewing): scroll history + if (term.sock_ready) TCPSocketSendString(term.sock, "\x1B[A"); + else TermViewScroll(1); + break; + case SC_CURSOR_DOWN: + if (term.sock_ready) TCPSocketSendString(term.sock, "\x1B[B"); + else TermViewScroll(-1); + break; + case SC_PAGE_UP: // Shift+PgUp scrolls history even while connected + if (term.sock_ready && !(sc & SCF_SHIFT)) TCPSocketSendString(term.sock, "\033[V"); + else TermViewScroll(term.window_height - 1); + break; + case SC_PAGE_DOWN: + if (term.sock_ready && !(sc & SCF_SHIFT)) TCPSocketSendString(term.sock, "\033[U"); + else TermViewScroll(-(term.window_height - 1)); + break; case SC_HOME: if (term.sock_ready) TCPSocketSendString(term.sock, "\033[H"); break; case SC_END: if (term.sock_ready) TCPSocketSendString(term.sock, "\033[K"); break; case SC_DELETE: if (term.sock_ready) TCPSocketSendString(term.sock, "\x7F"); break; @@ -416,18 +574,18 @@ init_connection: default: break; } + break; // was falling through and sending backspace codes on every TAB case CH_BACKSPACE: if (term.sock_ready) TCPSocketSendString(term.sock, "\x08\x7F"); break; case CH_SHIFT_ESC: - if (term.sock_ready) - { + if (term.sock) + { // covers both live connections and remote-closed sockets not yet freed term.waiting_for_input = TRUE; - term.sock_ready = 0; host = NULL; - TCPSocketClose(term.sock); - DocClear; - "Telnet connection closed.\n"; + TelnetDisconnect; + TermClear; + TermPutS("Telnet connection closed.\r\n"); Sleep(100); goto show_splash; } @@ -461,11 +619,11 @@ init_connection: catch PutExcept; - // ideally go back to the splashscreen and wait for another input? - // term.sock_ready = 0; - // term.waiting_for_input = TRUE; - // goto show_splash; + // Tear down: stop the receive task, then free any remaining socket. + force_disconnect = TRUE; + TelnetDisconnect; + Fs->draw_it = NULL; MenuPop; SettingsPop; GrPaletteSet(original_palette); @@ -484,8 +642,6 @@ Telnet; /* TODO: - make all keys send their respective signals (like Ctrl+Key, etc) -- refactor ANSIParser's TCPSocketSends to send everything to a "TelnetSend" function -- TelnetSend will handle whether there's a connection or not -- This will make easier to transform this software into an ANSI viewer / Telnet client/ general purpose Term. -- There's a lot more to fix, but this is a good start. +- underline/blink SGR attributes (needs extra per-cell flags) +- alternate screen buffer (CSI ?1049) */ diff --git a/src/Home/Net/Programs/Telnet/TelnetANSI.ZC b/src/Home/Net/Programs/Telnet/TelnetANSI.ZC index c62813bd8..fa1390d79 100644 --- a/src/Home/Net/Programs/Telnet/TelnetANSI.ZC +++ b/src/Home/Net/Programs/Telnet/TelnetANSI.ZC @@ -1,537 +1,399 @@ +// Terminal grid operations + ANSI CSI dispatch. +// All output mutates the term.screen_ch/screen_attr cell grid; nothing here +// touches DolDoc. TelnetDrawIt in Telnet.ZC blits the grid every frame, so +// in-place redraws (countdowns, progress bars, scrollers) render correctly. + +U8 ANSIColorToVGA(I64 c) +{ // ANSI SGR base color (0-7) to VGA palette index. Bright variant is | 8. + switch (c) + { + case 0: return BLACK; + case 1: return RED; + case 2: return GREEN; + case 3: return BROWN; + case 4: return BLUE; + case 5: return PURPLE; + case 6: return CYAN; + case 7: return LTGRAY; + } + return LTGRAY; +} + +U0 TermClear() +{ + MemSet(term.screen_ch, ' ', TERM_MAX_ROWS * TERM_MAX_COLS); + MemSet(term.screen_attr, term.cur_attr, TERM_MAX_ROWS * TERM_MAX_COLS); + term.csr_row = 0; + term.csr_col = 0; +} + +U0 TermScrollbackPush() +{ // top screen row is about to be evicted; save it into the history ring + MemCopy(term.back_ch + term.back_head * TERM_MAX_COLS, term.screen_ch, TERM_MAX_COLS); + MemCopy(term.back_attr + term.back_head * TERM_MAX_COLS, term.screen_attr, TERM_MAX_COLS); + + term.back_head = (term.back_head + 1) % TERM_SCROLLBACK_ROWS; + if (term.back_count < TERM_SCROLLBACK_ROWS) + term.back_count++; +} + +U0 TermScrollbackClear() +{ + term.back_head = 0; + term.back_count = 0; + term.view_offset = 0; +} + +U0 TermViewScroll(I64 delta) +{ // positive = further back into history; clamped to [0, back_count] + term.view_offset += delta; + if (term.view_offset < 0) + term.view_offset = 0; + if (term.view_offset > term.back_count) + term.view_offset = term.back_count; +} + +U0 TermScrollUp() +{ + I64 body = (term.window_height - 1) * TERM_MAX_COLS; + + TermScrollbackPush; + + MemCopy(term.screen_ch, term.screen_ch + TERM_MAX_COLS, body); + MemCopy(term.screen_attr, term.screen_attr + TERM_MAX_COLS, body); + MemSet(term.screen_ch + body, ' ', TERM_MAX_COLS); + MemSet(term.screen_attr + body, term.cur_attr, TERM_MAX_COLS); +} + +U0 TermScrollDown() +{ // rows move down; copy bottom-up because rows overlap in memory + I64 r; + + for (r = term.window_height - 1; r > 0; r--) + { + MemCopy(term.screen_ch + r * TERM_MAX_COLS, term.screen_ch + (r - 1) * TERM_MAX_COLS, TERM_MAX_COLS); + MemCopy(term.screen_attr + r * TERM_MAX_COLS, term.screen_attr + (r - 1) * TERM_MAX_COLS, TERM_MAX_COLS); + } + MemSet(term.screen_ch, ' ', TERM_MAX_COLS); + MemSet(term.screen_attr, term.cur_attr, TERM_MAX_COLS); +} + +U0 TermLineFeed() +{ + term.csr_row++; + if (term.csr_row >= term.window_height) + { + term.csr_row = term.window_height - 1; + TermScrollUp; + } +} + +U0 TermPutChar(U8 ch) +{ + I64 i; + + if (term.csr_col >= term.window_width) + { // deferred wrap: the cursor parks one past the edge until the next glyph, + // so art that paints exactly window_width columns per row lines up. + term.csr_col = 0; + TermLineFeed; + } + + i = term.csr_row * TERM_MAX_COLS + term.csr_col; + term.screen_ch[i] = ch; + term.screen_attr[i] = term.cur_attr; + term.csr_col++; +} + +U0 TermCursorSet(I64 row, I64 col) +{ + if (row < 0) + row = 0; + if (row >= term.window_height) + row = term.window_height - 1; + if (col < 0) + col = 0; + if (col >= term.window_width) + col = term.window_width - 1; + + term.csr_row = row; + term.csr_col = col; +} + +U0 TermEraseCells(I64 row, I64 col_start, I64 col_end) +{ // clear [col_start, col_end) of one row to spaces in the current attribute + I64 i = row * TERM_MAX_COLS; + + if (col_start < 0) + col_start = 0; + if (col_end > term.window_width) + col_end = term.window_width; + if (col_start >= col_end) + return; + + MemSet(term.screen_ch + i + col_start, ' ', col_end - col_start); + MemSet(term.screen_attr + i + col_start, term.cur_attr, col_end - col_start); +} + +U0 TermResize(I64 cols, I64 rows) +{ // resize terminal + its window. Runs from TerminalTask, so the window task + // handle must come from term.win_task, never Fs. + CTask *t = term.win_task; + + if (cols < 2 || rows < 2 || cols > TERM_MAX_COLS || rows > TERM_MAX_ROWS || !t) + return; + + term.window_width = cols; + term.window_height = rows; + + t->win_width = cols; + WinHorz((TEXT_COLS / 2) - (cols / 2), + (TEXT_COLS / 2) - (cols / 2) + cols - 1, + t); + t->win_height = rows; + WinVert((TEXT_ROWS / 2) - (rows / 2), + (TEXT_ROWS / 2) - (rows / 2) + rows - 1, + t); + + TermClear; +} + +U0 TermSGR(I64 *codes, I64 last_index) +{ // Select Graphic Rendition: colors + intensity. last_index inclusive. + I64 m, code; + + for (m = 0; m <= last_index; m++) + { + code = codes[m]; + + if (code == 0) + { // reset + term.cur_attr = TERM_DEFAULT_ATTR; + term.bold = FALSE; + } + else if (code == 1) + { // bold: brighten current foreground and later ones + term.bold = TRUE; + term.cur_attr |= 8; + } + else if (code == 2 || code == 22) + { + term.bold = FALSE; + term.cur_attr &= ~8; + } + else if (code == 7) + { // reverse video: swap fg/bg nibbles + term.cur_attr = term.cur_attr << 4 | term.cur_attr >> 4; + } + else if (code >= 30 && code <= 37) + { + term.cur_attr = term.cur_attr & 0xF0 | ANSIColorToVGA(code - 30); + if (term.bold) + term.cur_attr |= 8; + } + else if (code == 39) + { // default foreground + term.cur_attr = term.cur_attr & 0xF0 | TERM_DEFAULT_ATTR & 0x0F; + if (term.bold) + term.cur_attr |= 8; + } + else if (code >= 90 && code <= 97) + term.cur_attr = term.cur_attr & 0xF0 | ANSIColorToVGA(code - 90) | 8; + else if (code >= 40 && code <= 47) + term.cur_attr = term.cur_attr & 0x0F | ANSIColorToVGA(code - 40) << 4; + else if (code == 49) + term.cur_attr = term.cur_attr & 0x0F | TERM_DEFAULT_ATTR & 0xF0; + else if (code >= 100 && code <= 107) + term.cur_attr = term.cur_attr & 0x0F | (ANSIColorToVGA(code - 100) | 8) << 4; + // 3,4,5,8,9,...: italic/underline/blink/conceal unsupported; ignore + } +} + U0 HandleANSISequence(U8 *sequence) { - I64 ansi_code[MAX_ANSI_PARAMS]; - MemSet(ansi_code, 0, sizeof(ansi_code)); // Initialize all elements to 0 - I64 ansi_param_count = 0; - - U8 *ptr = sequence; - while (*ptr != '\0') { - if (IsDigit(*ptr)) { - ansi_code[ansi_param_count] = ansi_code[ansi_param_count] * 10 + (*ptr - '0'); - ptr++; - } else if (*ptr == ';') { - ansi_param_count++; - if (ansi_param_count >= MAX_ANSI_PARAMS) { - break; // Avoid going out of bounds - } - ptr++; - } else { - // This should be a letter indicating the end of the sequence - break; - } + I64 ansi_code[MAX_ANSI_PARAMS]; + I64 ansi_param_count = 0; + I64 n, m, row, col, code; + U8 actionCode; + U8 response_buf[16]; + U8 *ptr = sequence; + + MemSet(ansi_code, 0, sizeof(ansi_code)); + + while (*ptr != '\0') { + if (IsDigit(*ptr)) { + ansi_code[ansi_param_count] = ansi_code[ansi_param_count] * 10 + (*ptr - '0'); + ptr++; + } else if (*ptr == ';') { + ansi_param_count++; + if (ansi_param_count >= MAX_ANSI_PARAMS) { + break; // Avoid going out of bounds + } + ptr++; + } else { + // This should be the final byte (or a private marker like '?') + break; } + } + + if (ansi_param_count >= MAX_ANSI_PARAMS) + ansi_param_count = MAX_ANSI_PARAMS - 1; // loops below use m <= ansi_param_count + + actionCode = *ptr; + + switch (actionCode) + { + case 'A': // cursor up + n = ansi_code[0]; + if (!n) n = 1; + TermCursorSet(term.csr_row - n, term.csr_col); + break; + + case 'B': // cursor down + n = ansi_code[0]; + if (!n) n = 1; + TermCursorSet(term.csr_row + n, term.csr_col); + break; + + case 'C': // cursor right (non-destructive, unlike the old space-printing hack) + n = ansi_code[0]; + if (!n) n = 1; + TermCursorSet(term.csr_row, term.csr_col + n); + break; + + case 'D': // cursor left + n = ansi_code[0]; + if (!n) n = 1; + TermCursorSet(term.csr_row, term.csr_col - n); + break; + + case 'E': // cursor next line + n = ansi_code[0]; + if (!n) n = 1; + TermCursorSet(term.csr_row + n, 0); + break; + + case 'F': // cursor previous line + n = ansi_code[0]; + if (!n) n = 1; + TermCursorSet(term.csr_row - n, 0); + break; + + case 'G': // cursor horizontal absolute (1-based) + n = ansi_code[0]; + if (!n) n = 1; + TermCursorSet(term.csr_row, n - 1); + break; + + case 'H': // cursor position (1-based row;col) + case 'f': + row = ansi_code[0]; + col = ansi_code[1]; + if (!row) row = 1; + if (!col) col = 1; + TermCursorSet(row - 1, col - 1); + break; + + case 'J': // erase in display + switch (ansi_code[0]) + { + case 0: // cursor to end of screen + TermEraseCells(term.csr_row, term.csr_col, term.window_width); + for (m = term.csr_row + 1; m < term.window_height; m++) + TermEraseCells(m, 0, term.window_width); + break; + case 1: // start of screen to cursor + for (m = 0; m < term.csr_row; m++) + TermEraseCells(m, 0, term.window_width); + TermEraseCells(term.csr_row, 0, term.csr_col + 1); + break; + case 2: // whole screen; ANSI.SYS behavior homes the cursor too + TermClear; + break; + } + break; + + case 'K': // erase in line + switch (ansi_code[0]) + { + case 0: + TermEraseCells(term.csr_row, term.csr_col, term.window_width); + break; + case 1: + TermEraseCells(term.csr_row, 0, term.csr_col + 1); + break; + case 2: + TermEraseCells(term.csr_row, 0, term.window_width); + break; + } + break; + + case 'S': // scroll up n lines + n = ansi_code[0]; + if (!n) n = 1; + while (n--) + TermScrollUp; + break; + + case 'T': // scroll down n lines + n = ansi_code[0]; + if (!n) n = 1; + while (n--) + TermScrollDown; + break; + + case 'm': // colors / intensity + TermSGR(ansi_code, ansi_param_count); + break; + + case 's': // save cursor + term.saved_row = term.csr_row; + term.saved_col = term.csr_col; + break; + + case 'u': // restore cursor + TermCursorSet(term.saved_row, term.saved_col); + break; + + case 'n': + if (ansi_code[0] == 5) + { // device status: report OK + StrPrint(response_buf, "\x1B[0n"); + if (term.sock_ready) + TCPSocketSend(term.sock, response_buf, StrLen(response_buf)); + } + else if (ansi_code[0] == 6) + { // cursor position report: the real position now, not a hardcoded 24;80 + StrPrint(response_buf, "\x1B[%d;%dR", term.csr_row + 1, term.csr_col + 1); + if (term.sock_ready) + TCPSocketSend(term.sock, response_buf, StrLen(response_buf)); + } + break; + + case 'c': // device attributes: report as VT101 + StrPrint(response_buf, "\x1B[?1;0c"); + if (term.sock_ready) + TCPSocketSend(term.sock, response_buf, StrLen(response_buf)); + break; + + case '?': // private modes: ESC [ ? h|l + code = 0; + ptr++; // move past the marker + while (IsDigit(*ptr)) { + code = code * 10 + (*ptr - '0'); + ptr++; + } + if (code == 25) + term.csr_visible = (*ptr == 'h'); + // 47/1049 alternate screen, 7 wrap, etc: ignored + break; + + case 't': // ESC [ 8 ; rows ; cols t : resize request + if (ansi_code[0] == 8 && ansi_param_count >= 2) + TermResize(ansi_code[2], ansi_code[1]); + break; - // Now ptr should be at the final letter of the sequence - U8 actionCode = *ptr; - // Handle specific ANSI escape sequences - switch (actionCode) { - case 'n': - SysLog("Case n, %d\n",ansi_code[0]); - if (ansi_code[0] == 5) { - // Respond with terminal readiness - SysLog("reported terminal readiness\n"); - U8 deviceStatusResponse[5]; - deviceStatusResponse[0] = ANSI_ESC; - deviceStatusResponse[1] = ANSI_CSI; - deviceStatusResponse[2] = 0x30; // '0' - deviceStatusResponse[3] = 0x6E; // 'n' - deviceStatusResponse[4] = 0x00; // Null-terminator - if (term.sock_ready) TCPSocketSend(term.sock, deviceStatusResponse, 4); - } - else if (ansi_code[0] == 6) { - // Respond with cursor position - SysLog("reported cursor position\n"); - // TODO: position 24rows x 80cols is hardcoded, should actually report the real cursor position - // U8 cursorResponse[9] = "\x1B[24;80R"; - U8 cursorResponse[9]; - cursorResponse[0] = ANSI_ESC; - cursorResponse[1] = '['; // Start of CSI - cursorResponse[2] = '2'; // First digit of "24" - cursorResponse[3] = '4'; // Second digit of "24" - cursorResponse[4] = ';'; // Separator - cursorResponse[5] = '8'; // First digit of "80" - cursorResponse[6] = '0'; // Second digit of "80" - cursorResponse[7] = 'R'; // End of CPR - cursorResponse[8] = 0x00; // Null-terminator - if (term.sock_ready) TCPSocketSend(term.sock, cursorResponse, 9); - } - else if (ansi_code[0] == 255) { - // https://github.com/NuSkooler/enigma-bbs/blob/97cd0c3063b0c9f93a0fa4a44a85318ca81aef43/core/ansi_term.js#L140 - SysLog("TODO: reported screensize?\n"); - // SendWindowSize(term.sock, 80, 25); - } - break; - case 'c': - // Respond with device attributes - SysLog("reported device attributes\n"); - // Reports at VT101 (not sure why though) - U8 deviceAttributesResponse[8]; - deviceAttributesResponse[0] = ANSI_ESC; - deviceAttributesResponse[1] = ANSI_CSI; - deviceAttributesResponse[2] = 0x3F; // '?' - deviceAttributesResponse[3] = 0x31; // '1' - deviceAttributesResponse[4] = 0x3B; // ';' - deviceAttributesResponse[5] = 0x32; // '0' - deviceAttributesResponse[6] = 0x63; // 'c' - deviceAttributesResponse[7] = 0x00; // Null-terminator - if (term.sock_ready) TCPSocketSend(term.sock, deviceAttributesResponse, 7); - - break; - case 'm': - // this is where colors are being set - // TODO: what happens in this case??? --> [0;1;34;44m - I64 m; - Bool isBright = FALSE; - // if (ansi_param_count >2) - // { - // SysLog("param: %d \n", ansi_param_count); - // SysLog("%d | %d\n", m, ansi_code[m]); - // } - for (m = 0; m <= ansi_param_count; m++) { - if (ansi_code[m] <= 10) { - switch (ansi_code[m]) { - case 0: - // if (dark_mode) - // DocPrint(term.doc, "$$BG,WHITE$$$$BLACK$$"); - // else - // DocPrint(term.doc, "$$BG$$$$FG$$"); - DocPrint(term.doc, "$$BG,WHITE$$$$BLACK$$"); - isBright = FALSE; - break; // reset - case 1: isBright = TRUE; break; - case 2: isBright = FALSE; break; - default: break; - } - } - else if ((ansi_code[m] >= 30 && ansi_code[m] <= 39) || (ansi_code[m] >= 90 && ansi_code[m] <= 97)) { - // Set foreground color - // SysLog("ansi_code[%d] = %d\n", m, ansi_code[m]); - if(!isBright){ - switch (ansi_code[m]) { - case 30: - // if (dark_mode) DocPrint(term.doc, "$$WHITE$$"); - // else DocPrint(term.doc, "$$BLACK$$"); - DocPrint(term.doc, "$$WHITE$$"); - break; - case 31: - DocPrint(term.doc, "$$RED$$"); - break; - case 32: - DocPrint(term.doc, "$$GREEN$$"); - break; - case 33: - DocPrint(term.doc, "$$YELLOW$$"); - break; - case 34: - DocPrint(term.doc, "$$BLUE$$"); - break; - case 35: - DocPrint(term.doc, "$$PURPLE$$"); - break; - case 36: - DocPrint(term.doc, "$$CYAN$$"); - break; - case 37: - // if (dark_mode) DocPrint(term.doc, "$$BLACK$$"); - // else DocPrint(term.doc, "$$WHITE$$"); - DocPrint(term.doc, "$$BLACK$$"); - break; - case 39: - // if (dark_mode) DocPrint(term.doc, "$$WHITE$$"); - // else DocPrint(term.doc, "$$FG$$"); - DocPrint(term.doc, "$$WHITE$$"); - break; - default: break; - } - } - else { - switch (ansi_code[m]) { - case 90: - case 30: - // if (dark_mode) DocPrint(term.doc, "$$LTGRAY$$"); - // else DocPrint(term.doc, "$$DKGRAY$$"); - DocPrint(term.doc, "$$LTGRAY$$"); - break; - case 91: - case 31: - DocPrint(term.doc, "$$LTRED$$"); - break; - case 92: - case 32: - DocPrint(term.doc, "$$LTGREEN$$"); - break; - case 93: - case 33: - DocPrint(term.doc, "$$YELLOW$$"); - break; - case 94: - case 34: - DocPrint(term.doc, "$$LTBLUE$$"); - break; - case 95: - case 35: - DocPrint(term.doc, "$$LTPURPLE$$"); - break; - case 96: - case 36: - DocPrint(term.doc, "$$LTCYAN$$"); - break; - case 97: - case 37: - // if (dark_mode) DocPrint(term.doc, "$$DKGRAY$$"); - // else DocPrint(term.doc, "$$LTGRAY$$"); - DocPrint(term.doc, "$$DKGRAY$$"); - break; - case 39: - // if (dark_mode) DocPrint(term.doc, "$$WHITE$$"); - // else DocPrint(term.doc, "$$FG$$"); - DocPrint(term.doc, "$$WHITE$$"); - break; - default: break; - } - } - } - // this is a dumb approach, just do a CatPrint or something - // until we properly catch the `;` it will stay buggy - else if ((ansi_code[m] >= 40 && ansi_code[m] <= 49) || (ansi_code[m] >= 100 && ansi_code[m] <= 107)) { - // Set background color - // SysLog("ansi_code[%d] = %d\n", m, ansi_code[m]); - if(!isBright){ - switch (ansi_code[m]) { - case 40: - // if (dark_mode) DocPrint(term.doc, "$$BG,WHITE$$"); - // else DocPrint(term.doc, "$$BG,BLACK$$"); - DocPrint(term.doc, "$$BG,WHITE$$"); - break; - case 41: - DocPrint(term.doc,"$$BG,RED$$"); - break; - case 42: - DocPrint(term.doc,"$$BG,GREEN$$"); - break; - case 43: - DocPrint(term.doc,"$$BG,YELLOW$$"); - break; - case 44: - DocPrint(term.doc,"$$BG,BLUE$$"); - break; - case 45: - DocPrint(term.doc,"$$BG,PURPLE$$"); - break; - case 46: - DocPrint(term.doc,"$$BG,CYAN$$"); - break; - case 47: - // if (dark_mode) DocPrint(term.doc, "$$BG,BLACK$$"); - // else DocPrint(term.doc, "$$BG,WHITE$$"); - DocPrint(term.doc, "$$BG,BLACK$$"); - break; - case 49: - // if (dark_mode) DocPrint(term.doc, "$$BG,WHITE$$"); - // else DocPrint(term.doc, "$$BG$$"); - DocPrint(term.doc, "$$BG,WHITE$$"); - break; - default: break; - } - } - else { - switch (ansi_code[m]) { - case 100: - case 40: - // if (dark_mode) DocPrint(term.doc, "$$BG,LTGRAY$$"); - // else DocPrint(term.doc, "$$BG,DKGRAY$$"); - DocPrint(term.doc, "$$BG,LTGRAY$$"); - break; - case 101: - case 41: - DocPrint(term.doc,"$$BG,LTRED$$"); - break; - case 102: - case 42: - DocPrint(term.doc,"$$BG,LTGREEN$$"); - break; - case 103: - case 43: - DocPrint(term.doc,"$$BG,YELLOW$$"); - break; - case 104: - case 44: - DocPrint(term.doc,"$$BG,LTBLUE$$"); - break; - case 105: - case 45: - DocPrint(term.doc,"$$BG,LTPURPLE$$"); - break; - case 106: - case 46: - DocPrint(term.doc,"$$BG,LTCYAN$$"); - break; - case 107: - case 47: - // if (dark_mode) DocPrint(term.doc, "$$BG,DKGRAY$$"); - // else DocPrint(term.doc, "$$BG,LTGRAY$$"); - DocPrint(term.doc, "$$BG,DKGRAY$$"); - break; - case 49: - // if (dark_mode) DocPrint(term.doc, "$$BG,LTGRAY$$"); - // else DocPrint(term.doc, "$$BG$$"); - DocPrint(term.doc, "$$BG,LTGRAY$$"); - break; - // reset - default: break; - } - } - } - } - - break; - case 'A': - // Cursor Up - SysLog("Cursor Up\n"); - // "$$CM+TY,0,-%d$$", ansi_code[0]; - DocPrint(term.doc, "$$CM,0,-%d$$", ansi_code[0]); - - break; - case 'B': - // Cursor Down - SysLog("Cursor Down\n"); - DocPrint(term.doc, "$$CM,0,%d$$", ansi_code[0]); - - break; - case 'C': - // Cursor Right - // SysLog("Cursor Right %d %d\n", ansi_param_count, ansi_code[0]); - // DocPrint(term.doc, "$$CM,%d,0$$", ansi_code[0]); - // seems to be less prone to bugs? - I64 cr; - for (cr=0; cr= term.window_height) { - row = term.window_height - 1; - } else if (row < 1) { - row = 1; - } - - if (col >= term.window_width) { - col = term.window_width - 1; - } else if (col < 1) { - col = 1; - } - - // if (row == term.window_height) { - // term.current_row = row; - // term.current_col = col; - // DocPrint(term.doc, "$$CM+LX+TY,LE=%d,RE=%d$$", term.current_col-1, term.current_row-1); - // DocPrint(term.doc, "\n"); - // - // break; - // } - - // If row or col are at their max value, reset the current position to 1 - // if (row == term.window_height || col == term.window_width) { - // if (row == term.window_height) term.current_row = 1; - // if (col == term.window_width) term.current_col = 1; - // - // break; - // } - - term.current_row = row; - term.current_col = col; - - DocPrint(term.doc, "$$CM+LX+TY,LE=%d,RE=%d$$", term.current_col-1, term.current_row-1); - - break; - case 'J': - SysLog("J code, %d %d\n", ansi_param_count, ansi_code[0]); - // Erase in Display - if (ansi_code[0] == 0) { - // Erase from cursor to end of display - // TODO: dont think this is working - // DocDelToNum(Fs->display_doc, Fs->display_doc->cur_entry->line_num); - } else if (ansi_code[0] == 1) { - // Erase from cursor to beginning of display - // TODO: dont think this is working - // DocDelToEntry(Fs->display_doc, Fs->display_doc->cur_entry, FALSE); - } else if (ansi_code[0] == 2) { - // Erase entire display - DocClear(term.doc); - // Clear the buffer - // term.buffer_len = 0; - // MemSet(term.buffer, 0, BUF_SIZE); - // Sleep(100); - } - - break; - case 'K': - // TODO: I have no idea if this actually works - SysLog("K code, %d %d\n", ansi_param_count, ansi_code[0]); - if (ansi_param_count == 0 || ansi_code[0] == 0) { - // Erase from cursor to end of line - // LineDeleteToEnd(term.doc->cur_entry, doc.term->cur_col); - // DocDelToEntry(term.doc, term.doc->cur_entry, TRUE); - - // DocDelToNum(term.doc, 80); - // DocRecalc(term.doc); - } else if (ansi_code[0] == 1) { - // Erase from cursor to beginning of line - // LineDeleteToStart(term.doc->cur_entry, doc.term->cur_col); - } else if (ansi_code[0] == 2) { - // Erase entire line - // LineDeleteEntire(term.doc->cur_entry); - } - - break; - case 'L': - SysLog("L code\n"); - // DocPrint(term.doc, "\n"); - - break; - case 'S': - // TODO: Scroll Up - SysLog("S Scroll Up"); - - break; - case 'T': - // TODO: Scroll Down - SysLog("T Scroll Down"); - - break; - case 'M': - SysLog("Case M\n"); - // TODO: is this correct? cursor should go one line up - DocPrint(term.doc, "$$CM,0,-1$$"); - - break; - case '?': - - I64 code = 0; - - while (IsDigit(*ptr)) { - code = code * 10 + (*ptr - '0'); - - } - switch (code) { - case 25: - // need to specify which doc? - // if (*ptr == 'l') DocCursor(OFF); // Hide cursor - // if (*ptr == 'h') DocCursor(ON); // Show cursor - // Move past 'l' or 'h' - break; - case 47: - if (*ptr == 'l') SysLog("code 47l\n"); // restore screen - if (*ptr == 'h') SysLog("code 47h\n"); // save screen - // Move past 'l' or 'h' - break; - case 1049: - if (*ptr == 'l') SysLog("code 1049l\n"); // enables the alternative buffer - if (*ptr == 'h') SysLog("code 1049h\n"); // disables the alternative buffer - // Move past 'l' or 'h' - break; - default: - - break; - } - break; - case 's': - SysLog("SaveCurrentCursorPosition\n"); - - break; - case 'u': - SysLog("RestoreCurrentCursorPosition\n"); - - break; - case 'r': - // self.restoreCursorPosition(); - SysLog("r case \n"); - - break; - case 'h': - case 'l': - // TODO: Handle 'h' (set mode) or 'l' (reset mode) codes - SysLog("h or l case \n"); - // Skip 'h' or 'l' - break; - case 't': - // for (m = 0; m <= ansi_param_count; m++) { - SysLog("ansi_code[%d]: %d\n", m, ansi_code[m]); - // } - if (ansi_param_count == 3) { - if (ansi_code[0] == 8) { - term.window_width = ansi_code[1]; - term.window_height = ansi_code[2]; - Fs->win_width = term.window_width; - WinHorz((TEXT_COLS / 2) - (Fs->win_width / 2), - (TEXT_COLS / 2) - (Fs->win_width / 2) + - (Fs->win_width - 1), - Fs); - Fs->win_height = term.window_height; - WinVert((TEXT_ROWS / 2) - (Fs->win_height / 2), - (TEXT_ROWS / 2) - (Fs->win_height / 2) + - (Fs->win_height - 1), - Fs); - } - } - - - break; - case '=': - SysLog("ScreenMode attempt\n"); - - break; - default: - // if(!IsDigit(*ptr)) { - if(*ptr > 0x32) { - SysLog("Unknown code: 0x%X\n", *ptr); - } - - break; - } - + default: // L/M insert/delete line, h/l modes, =, r margins: ignored + break; + } } diff --git a/src/Home/Net/Programs/Telnet/TelnetClass.ZC b/src/Home/Net/Programs/Telnet/TelnetClass.ZC index 067ea5e51..9fde9ba5a 100755 --- a/src/Home/Net/Programs/Telnet/TelnetClass.ZC +++ b/src/Home/Net/Programs/Telnet/TelnetClass.ZC @@ -1,16 +1,58 @@ +// Telnet stream parser states. State persists across TCPSocketReceive calls, +// so telnet commands and ANSI sequences split across TCP segments parse correctly. +#define TELNET_STATE_NORMAL 0 // plain data +#define TELNET_STATE_IAC 1 // saw IAC, expect command byte +#define TELNET_STATE_IAC_OPT 2 // saw WILL/WONT/DO/DONT, expect option byte +#define TELNET_STATE_SB 3 // inside subnegotiation, collecting payload +#define TELNET_STATE_SB_IAC 4 // saw IAC inside subnegotiation +#define TELNET_STATE_ESC 5 // saw ESC +#define TELNET_STATE_CSI 6 // saw ESC [, collecting until final byte +#define TELNET_STATE_ESC_CHARSET 7 // saw ESC ( or ESC ), swallow designation byte + +#define TELNET_SEQ_BUF_SIZE 64 // ANSI CSI parameter collect buffer +#define TELNET_SB_BUF_SIZE 64 // telnet subnegotiation payload buffer + +// The terminal proper: a fixed grid of cells, one glyph + one color attribute +// each, exactly like VGA text mode. attr = background << 4 | foreground, +// both VGA palette indices. Rendered by TelnetDrawIt each window refresh. +#define TERM_MAX_COLS 132 +#define TERM_MAX_ROWS 60 + +#define TERM_SCROLLBACK_ROWS 600 // history ring; rows evicted off the top land here + +#define TERM_DEFAULT_ATTR 0x07 // LTGRAY on BLACK, classic ANSI default + class Terminal { I64 sock; Bool sock_ready; + Bool task_idle; // TRUE while TerminalTask is not touching sock; gates safe socket close Bool waiting_for_input; - I64 window_width; + I64 window_width; // active terminal size in cells (<= TERM_MAX_*) I64 window_height; CDoc *doc; - CTask *task; + CTask *task; // TerminalTask (receiver) + CTask *win_task; // the Telnet window's task; resize must target this, not Fs of the caller + + U8 screen_ch[TERM_MAX_ROWS * TERM_MAX_COLS]; // row-major, stride TERM_MAX_COLS + U8 screen_attr[TERM_MAX_ROWS * TERM_MAX_COLS]; + + U8 back_ch[TERM_SCROLLBACK_ROWS * TERM_MAX_COLS]; // scrollback ring buffer + U8 back_attr[TERM_SCROLLBACK_ROWS * TERM_MAX_COLS]; + I64 back_head; // next ring row to write + I64 back_count; // filled ring rows + I64 view_offset; // rows scrolled back into history; 0 = live view + I64 csr_row, csr_col; + I64 saved_row, saved_col; // for CSI s/u and ESC 7/8 + U8 cur_attr; // attribute applied to newly written cells + Bool bold; // SGR 1 active: foregrounds get the bright bit + Bool csr_visible; // toggled by CSI ?25h / ?25l - I64 current_color; - I64 current_bgcolor; - I64 current_row; - I64 current_col; + I64 parse_state; // TELNET_STATE_* + U8 iac_cmd; // pending WILL/WONT/DO/DONT command byte + U8 seq_buf[TELNET_SEQ_BUF_SIZE]; + I64 seq_len; + U8 sb_buf[TELNET_SB_BUF_SIZE]; + I64 sb_len; U8 buffer[BUF_SIZE]; I64 buffer_len; diff --git a/src/Home/Net/Programs/Telnet/TelnetHelpers.ZC b/src/Home/Net/Programs/Telnet/TelnetHelpers.ZC index 937b2a501..48c05cb8b 100755 --- a/src/Home/Net/Programs/Telnet/TelnetHelpers.ZC +++ b/src/Home/Net/Programs/Telnet/TelnetHelpers.ZC @@ -1,6 +1,5 @@ -Cd(__DIR__); -#include "TelnetClass" -#include "TelnetNegotiation" +// Include order is managed by Telnet.ZC: +// TelnetClass -> TelnetHelpers -> TelnetNegotiation -> TelnetANSI. U8 IsDigit(U8 ch) { return '0' <= ch <= '9'; @@ -26,10 +25,9 @@ I64 ANSIArtLoad(U8 *filename, U8 *buffer) { return -1; } - // Allocate memory for the buffer based on file size - *buffer = MAlloc(file->de.size); - if (!(*buffer)) { - PrintErr("Failed to allocate memory for the buffer"); + // buffer is the caller's fixed BUF_SIZE array; refuse files that don't fit. + if (file->de.size > BUF_SIZE - 1) { + PrintErr("ANSI art file too big: %d bytes (max %d)", file->de.size, BUF_SIZE - 1); FClose(file); return -1; } diff --git a/src/Home/Net/Programs/Telnet/TelnetNegotiation.ZC b/src/Home/Net/Programs/Telnet/TelnetNegotiation.ZC index 402508397..a5d819dd5 100755 --- a/src/Home/Net/Programs/Telnet/TelnetNegotiation.ZC +++ b/src/Home/Net/Programs/Telnet/TelnetNegotiation.ZC @@ -14,40 +14,9 @@ #define SE 0xF0 #define CHARSET 0x2A -// #define TELNET_IAC 255 /* 0xff - Interpret as command */ -// #define TELNET_DONT 254 /* 0xfe - Don't do option */ -// #define TELNET_DO 253 /* 0xfd - Do option */ -// #define TELNET_WONT 252 /* 0xfc - Won't do option */ -// #define TELNET_WILL 251 /* 0xfb - Will do option */ - -// #define TELNET_SB 250 /* 0xfa - sub-negotiation */ -// #define TELNET_GA 249 /* 0xf9 - Go ahead */ -// #define TELNET_EL 248 /* 0xf8 - Erase line */ -// #define TELNET_EC 247 /* 0xf7 - Erase char */ -// #define TELNET_AYT 246 /* 0xf6 - Are you there? */ -// #define TELNET_AO 245 /* 0xf5 - Abort output */ -// #define TELNET_IP 244 /* 0xf4 - Interrupt process */ -// #define TELNET_BRK 243 /* 0xf3 - Break */ -// #define TELNET_SYNC 242 /* 0xf2 - Data mark */ -// #define TELNET_NOP 241 /* 0xf1 - No operation */ -#define SE_END 240 - -// #define TELNET_NAWS 31 -// #define TELNET_TERMINAL_TYPE 24 -#define SEND_LOCATION 23 +#define SEND_LOCATION 23 #define BINARY_TRANSMISSION 0 - - -// U0 TelnetRequest(I64 sock, U8 option_code) -// { -// U8 request[3]; -// request[0] = IAC; -// request[1] = WILL; -// request[2] = option_code; - -// TCPSocketSend(sock, request, 3); -// } U0 SendWindowSize(I64 sock, U16 cols, U16 rows) { U8 buf[9]; @@ -55,7 +24,7 @@ U0 SendWindowSize(I64 sock, U16 cols, U16 rows) buf[0] = IAC; buf[1] = SB; buf[2] = NAWS; - // if dynamic resolution, make sure to account for 255 AIC (byte stuffing needs to be done) + // if dynamic resolution, make sure to account for 255 IAC (byte stuffing needs to be done) buf[3] = cols >> 8; // High byte of columns buf[4] = cols & 0xFF; // Low byte of columns buf[5] = rows >> 8; // High byte of rows @@ -69,7 +38,6 @@ U0 SendWindowSize(I64 sock, U16 cols, U16 rows) U0 SendTerminalType(I64 sock) { - U8 response[14]; response[0] = IAC; @@ -90,113 +58,87 @@ U0 SendTerminalType(I64 sock) response[13] = SE; SysLog("SendTerminalType: ANSI-BBS\n"); - TCPSocketSend(sock, response, 14); + TCPSocketSend(sock, response, 14); } -U0 TelnetNegotiate(I64 sock, U8 *ptr) -{ - U8 negotiation_code = *(ptr + 1); - U8 option_code = *(ptr + 2); - - - SysLog("Negotiation code: %d | Option code: %d\n", negotiation_code, option_code); - - // Check if this is a subnegotiation request - if (negotiation_code == SB) - { - SysLog("SB: %d\n", option_code); - if (option_code == TERMINAL_TYPE && *(ptr + 3) == SEND) - { - SendTerminalType(sock); - } - else if (option_code == NAWS && *(ptr + 3) == SEND) - { - SendWindowSize(sock, 80, 25); - } - return; - } - - // Otherwise, handle it as a normal negotiation... +U0 TelnetNegotiateOption(I64 sock, U8 negotiation_code, U8 option_code) +{ // Respond to a 3-byte negotiation: IAC