From 2a1bc83e93911d1b17939b68c1f6035e57d8d160 Mon Sep 17 00:00:00 2001
From: Nigel Reed <nigel@nigelreed.net>
Date: Tue, 11 Feb 2025 01:29:44 -0600
Subject: [PATCH] Initial changes to support MODE7 graphics natively Similar to
 PETSCII it will answer on a dedicated port.

---
 ctrl/sbbs.ini             |  4 +++-
 ctrl/text.dat             |  2 ++
 exec/load/termdesc.js     |  6 ++++++
 exec/load/text.js         |  4 +++-
 exec/load/userdefs.js     |  2 +-
 exec/login.js             |  2 +-
 exec/logon.js             |  6 ++++++
 exec/logonlist.js         |  7 +++++++
 src/sbbs3/answer.cpp      |  3 +++
 src/sbbs3/ars.c           |  4 ++++
 src/sbbs3/ars_defs.h      |  2 ++
 src/sbbs3/chk_ar.cpp      | 14 ++++++++++++++
 src/sbbs3/con_out.cpp     | 12 ++++++++++++
 src/sbbs3/logon.cpp       |  4 ++--
 src/sbbs3/main.cpp        |  7 +++++++
 src/sbbs3/newuser.cpp     |  3 +++
 src/sbbs3/prntfile.cpp    |  5 +++++
 src/sbbs3/sbbs.h          |  2 ++
 src/sbbs3/sbbs_ini.c      |  6 ++++++
 src/sbbs3/sbbsdefs.h      |  6 ++++--
 src/sbbs3/scfg/scfgsrvr.c | 31 +++++++++++++++++++------------
 src/sbbs3/startup.h       |  1 +
 src/sbbs3/text.h          |  2 ++
 src/sbbs3/text_defaults.c |  2 ++
 src/sbbs3/text_id.c       |  2 ++
 src/sbbs3/userdat.c       |  6 ++++++
 src/sbbs3/useredit.cpp    |  2 +-
 src/sbbs3/xtrn.cpp        |  9 +++++++++
 28 files changed, 135 insertions(+), 21 deletions(-)

diff --git a/ctrl/sbbs.ini b/ctrl/sbbs.ini
index df19c8ae08..c47ddf1caa 100644
--- a/ctrl/sbbs.ini
+++ b/ctrl/sbbs.ini
@@ -77,7 +77,9 @@
 	Pet40Port = 64
 	; TCP port for 80-column PETSCII connections (any terminal protocol):
 	Pet80Port = 128
-	; Note on PETSCII support: you must add the same port(s) to one of your
+	; TCP port for 40-column MODE7 connections (any termnial protocol):
+	Mode7Port = 5050
+	; Note on PETSCII and MODE7 support: you must add the same port(s) to one of your
 	; *Interface= values above to open/listen/accept connections on that port.
 	; Example:
 	;   TelnetInterface = 71.95.196.34,71.95.196.34:64,71.95.196.34:128
diff --git a/ctrl/text.dat b/ctrl/text.dat
index 7a9f5d4c73..424225c7e1 100644
--- a/ctrl/text.dat
+++ b/ctrl/text.dat
@@ -1124,3 +1124,5 @@
 " %c \1g%.10s\1n %c %.127s%c"							   935 QWKTagLineFmt
 "\1n\r\n\1b\1hQWK Control [\1c%s\1b]: \1g%s\r\n"		   936 QWKControlCommand
 "Unrecognized Control Command!\1n\r\n"					   937 QWKBadControlCommand
+"Mode 7 terminal detected"								   938 Mode7TerminalDetected
+"Are you using a Mode 7 terminal"						   939 Mode7TerminalQ
diff --git a/exec/load/termdesc.js b/exec/load/termdesc.js
index c514c06524..ed0135707a 100644
--- a/exec/load/termdesc.js
+++ b/exec/load/termdesc.js
@@ -22,6 +22,8 @@ function charset(term)
 		term = console.term_supports();
 	if(term & USER_PETSCII)
 		return "CBM-ASCII";
+	if(term & USER_MODE7)
+		return "MODE7";
 	if(term & USER_UTF8)
 		return "UTF-8";
 	if(term & USER_NO_EXASCII)
@@ -62,6 +64,8 @@ function type(verbose, usr)
 	var type = "DUMB";
 	if(term & USER_PETSCII)
 		type = "PETSCII";
+	if(term & USER_MODE7)
+		type = "MODE7";
 	if(term & USER_RIP)
 		type = "RIP";
 	if(term & USER_ANSI)
@@ -72,6 +76,8 @@ function type(verbose, usr)
 	// Verbose
 	if(term & USER_PETSCII)
 		return ((usr.settings & USER_AUTOTERM) ? bbs.text(TerminalAutoDetect) : "") + "CBM/PETSCII";
+	if(term & USER_MODE7)
+		return ((usr.settings & USER_AUTOTERM) ? bbs.text(TerminalAutoDetect) : "") + "MODE7";
 	return format("%s%s / %s %s%s%s"
 		,(usr.settings & USER_AUTOTERM) ? bbs.text(TerminalAutoDetect) : ""
 		,this.charset(term)
diff --git a/exec/load/text.js b/exec/load/text.js
index ab3fc80346..a780a76474 100644
--- a/exec/load/text.js
+++ b/exec/load/text.js
@@ -944,7 +944,9 @@ var QWKEndOfMessage=934;
 var QWKTagLineFmt=935;
 var QWKControlCommand=936;
 var QWKBadControlCommand=937;
+var Mode7TerminalDetected=938;
+var Mode7TerminalQ=939;
 
-var TOTAL_TEXT=938;
+var TOTAL_TEXT=940;
 
 this;
diff --git a/exec/load/userdefs.js b/exec/load/userdefs.js
index 4f7aad07d7..d09319efa7 100644
--- a/exec/load/userdefs.js
+++ b/exec/load/userdefs.js
@@ -24,7 +24,7 @@ const USER_AUTOTERM	    = (1<<17);	// Autodetect terminal type
 const USER_COLDKEYS	    = (1<<18);	// No hot-keys								
 const USER_EXTDESC 	    = (1<<19);	// Extended file descriptions				
 const USER_AUTOHANG	    = (1<<20);	// Auto-hang-up after transfer				
-const USER_WIP 		    = (1<<21);	// Supports WIP terminal emulation			
+const USER_MODE7	    = (1<<21);	// BBC Micro Mode 7 terminal support
 const USER_AUTOLOGON    = (1<<22);	// AutoLogon via IP							
 const USER_HTML		    = (1<<23);	// Using Deuce's HTML terminal (*cough*)	
 const USER_NOPAUSESPIN  = (1<<24);	// No spinning cursor at pause prompt		
diff --git a/exec/login.js b/exec/login.js
index a4c7324916..347eb97e9e 100644
--- a/exec/login.js
+++ b/exec/login.js
@@ -27,7 +27,7 @@ if(!bbs.online)
 	exit();
 var inactive_hangup = parseInt(options.inactive_hangup, 10);
 if(inactive_hangup && inactive_hangup < console.max_socket_inactivity
-	&& !(console.autoterm&(USER_ANSI | USER_PETSCII | USER_UTF8))) {
+	&& !(console.autoterm&(USER_ANSI | USER_PETSCII | USER_UTF8 | USER_MODE7))) {
 	console.max_socket_inactivity = inactive_hangup;
 	log(LOG_NOTICE, "terminal not detected, reducing inactivity hang-up timeout to " + console.max_socket_inactivity + " seconds");
 }
diff --git a/exec/logon.js b/exec/logon.js
index 6094ea5dda..5be63f0299 100644
--- a/exec/logon.js
+++ b/exec/logon.js
@@ -11,6 +11,12 @@
 
 require("sbbsdefs.js", 'SS_RLOGIN');
 require("nodedefs.js", 'NODE_QUIET');
+
+if(console.term_supports(USER_MODE7)) {
+	log(LOG_DEBUG, "Setting users language to m7");
+	user.lang = "m7";
+}
+
 if(!bbs.mods.avatar_lib)
 	bbs.mods.avatar_lib = load({}, 'avatar_lib.js');
 if(!bbs.mods.logonlist_lib)
diff --git a/exec/logonlist.js b/exec/logonlist.js
index da7f527347..63b174aa0b 100644
--- a/exec/logonlist.js
+++ b/exec/logonlist.js
@@ -45,6 +45,13 @@ if(!js.global.bbs) {
 if(!bbs.mods.logonlist_lib)
 	bbs.mods.logonlist_lib = load({}, 'logonlist_lib.js');
 var options = load("modopts.js", "logonlist");
+function dump_objs(obj) {
+        Object.keys(obj).forEach(function (e) {
+                writeln(e + ': ' + JSON.stringify(obj[e]));
+        });
+}
+dump_objs(options);
+writeln("lang: " + user.lang);
 if(!options)
 	options = {};
 if(options.last_few_callers === undefined)
diff --git a/src/sbbs3/answer.cpp b/src/sbbs3/answer.cpp
index 2c0e2921b3..62a45ca9c9 100644
--- a/src/sbbs3/answer.cpp
+++ b/src/sbbs3/answer.cpp
@@ -553,6 +553,9 @@ bool sbbs_t::answer()
 			SAFECOPY(terminal, "PETSCII");
 			outchar(FF);
 			center(str);
+		} else if (autoterm & MODE7) {
+			SAFECOPY(terminal, "MODE7");
+			center(str);
 		} else {    /* ANSI+ terminal detection */
 			putcom( "\r\n"      /* locate cursor at column 1 */
 			        "\x1b[s"    /* save cursor position (necessary for HyperTerm auto-ANSI) */
diff --git a/src/sbbs3/ars.c b/src/sbbs3/ars.c
index 9f1a995d16..2719f0a12b 100644
--- a/src/sbbs3/ars.c
+++ b/src/sbbs3/ars.c
@@ -337,6 +337,10 @@ uchar* arstr(ushort* count, const char* str, scfg_t* cfg, uchar* ar_buf)
 				artype = AR_PETSCII;
 				i += 6;
 			}
+			else if (!strnicmp(str + i, "MODE7", 5)) {
+				artype = AR_MODE7;
+				i += 4;
+			}
 			else if (!strnicmp(str + i, "ASCII", 5)) {
 				artype = AR_ASCII;
 				i += 4;
diff --git a/src/sbbs3/ars_defs.h b/src/sbbs3/ars_defs.h
index 9d4c003bd5..fa5de50605 100644
--- a/src/sbbs3/ars_defs.h
+++ b/src/sbbs3/ars_defs.h
@@ -116,6 +116,7 @@ enum {                              /* Access requirement binaries */
 	, AR_UTF8
 	, AR_CP437
 	, AR_USERNAME
+	, AR_MODE7
 };
 
 enum ar_type {
@@ -148,6 +149,7 @@ static inline enum ar_type ar_type(int artype)
 		case AR_GUEST:
 		case AR_QNODE:
 		case AR_QUIET:
+		case AR_MODE7:
 			return AR_BOOL;
 		case AR_SUBCODE:
 		case AR_DIRCODE:
diff --git a/src/sbbs3/chk_ar.cpp b/src/sbbs3/chk_ar.cpp
index 731c0796b7..ca0e4e7189 100644
--- a/src/sbbs3/chk_ar.cpp
+++ b/src/sbbs3/chk_ar.cpp
@@ -128,6 +128,20 @@ bool sbbs_t::ar_exp(const uchar **ptrptr, user_t* user, client_t* client)
 					noaccess_val = PETSCII;
 				}
 				break;
+			case AR_MODE7:
+				{
+					int val = term_supports();
+					val &= CHARSET_FLAGS;
+					if (val != CHARSET_MODE7)
+						result = _not;
+					else
+						result = !_not;
+					if (!result) {
+						noaccess_str = text[NoAccessTerminal];
+						noaccess_val = MODE7;
+					}
+				}
+				break;
 			case AR_ASCII:
 				if ((term_supports() & CHARSET_FLAGS) != CHARSET_ASCII)
 					result = _not;
diff --git a/src/sbbs3/con_out.cpp b/src/sbbs3/con_out.cpp
index eda850dfd5..c1b99074af 100644
--- a/src/sbbs3/con_out.cpp
+++ b/src/sbbs3/con_out.cpp
@@ -245,6 +245,11 @@ unsigned char cp437_to_petscii(unsigned char ch)
 	return ch;
 }
 
+unsigned char cp437_to_mode7(unsigned char ch)
+{
+	return ch;
+}
+
 /* Perform PETSCII conversion to ANSI-BBS/CP437 */
 int sbbs_t::petscii_to_ansibbs(unsigned char ch)
 {
@@ -539,6 +544,9 @@ char* sbbs_t::term_type(user_t* user, int term, char* str, size_t size)
 	if (term & PETSCII)
 		safe_snprintf(str, size, "%sCBM/PETSCII"
 		              , (user->misc & AUTOTERM) ? text[TerminalAutoDetect] : nulstr);
+	else if (term & MODE7)
+		safe_snprintf(str, size, "%s/MODE7"
+		              , (user->misc & AUTOTERM) ? text[TerminalAutoDetect] : nulstr);
 	else
 		safe_snprintf(str, size, "%s%s / %s %s%s%s"
 		              , (user->misc & AUTOTERM) ? text[TerminalAutoDetect] : nulstr
@@ -560,6 +568,8 @@ const char* sbbs_t::term_type(int term)
 		term = term_supports();
 	if (term & PETSCII)
 		return "PETSCII";
+	if (term & MODE7)
+		return "MODE7";
 	if (term & RIP)
 		return "RIP";
 	if (term & ANSI)
@@ -576,6 +586,8 @@ const char* sbbs_t::term_charset(int term)
 		term = term_supports();
 	if (term & PETSCII)
 		return "CBM-ASCII";
+	if (term & MODE7)
+		return "MODE7";
 	if (term & UTF8)
 		return "UTF-8";
 	if (term & NO_EXASCII)
diff --git a/src/sbbs3/logon.cpp b/src/sbbs3/logon.cpp
index 5c3022c0ca..f9231b0929 100644
--- a/src/sbbs3/logon.cpp
+++ b/src/sbbs3/logon.cpp
@@ -182,8 +182,8 @@ bool sbbs_t::logon()
 	}
 
 	if ((useron.misc & AUTOTERM)
-	    // User manually-enabled PETSCII, but they're logging in with an ANSI (auto-detected) terminal
-	    || ((useron.misc & PETSCII) && (autoterm & ANSI))) {
+	    // User manually-enabled PETSCII or MODE7, but they're logging in with an ANSI (auto-detected) terminal
+	    || (((useron.misc & PETSCII) || (useron.misc & MODE7)) && (autoterm & ANSI))) {
 		useron.misc &= ~(ANSI | RIP | CHARSET_FLAGS);
 		useron.misc |= (AUTOTERM | autoterm);
 	}
diff --git a/src/sbbs3/main.cpp b/src/sbbs3/main.cpp
index 20bea4fad6..2b6feda0e5 100644
--- a/src/sbbs3/main.cpp
+++ b/src/sbbs3/main.cpp
@@ -24,6 +24,7 @@
 #include "telnet.h"
 #include "netwrap.h"
 #include "petdefs.h"
+#include "mode7defs.h"
 #include "filedat.h"
 #include "js_rtpool.h"
 #include "js_request.h"
@@ -5663,6 +5664,12 @@ NO_SSH:
 				sbbs->outcom(PETSCII_UPPERLOWER);
 			}
 
+			if (inet_addrport(&local_addr) == startup->mode7_port) {
+				sbbs->autoterm = MODE7;
+				sbbs->cols = 40;
+				sbbs->rows = 25;
+			}
+
 			sbbs->bprintf("\r\n%s\r\n", VERSION_NOTICE);
 			sbbs->bprintf("%s connection from: %s\r\n", client.protocol, host_ip);
 
diff --git a/src/sbbs3/newuser.cpp b/src/sbbs3/newuser.cpp
index 7775414cbc..e46c3b31bb 100644
--- a/src/sbbs3/newuser.cpp
+++ b/src/sbbs3/newuser.cpp
@@ -153,6 +153,9 @@ bool sbbs_t::newuser()
 			autoterm |= PETSCII;
 			outcom(PETSCII_UPPERLOWER);
 			bputs(text[PetTerminalDetected]);
+		} else if (useron.misc & MODE7) {
+			autoterm |= MODE7;
+			bputs(text[Mode7TerminalDetected]);
 		} else {
 			if (!yesno(text[ExAsciiTerminalQ]))
 				useron.misc |= NO_EXASCII;
diff --git a/src/sbbs3/prntfile.cpp b/src/sbbs3/prntfile.cpp
index caf470b4d5..71fc770859 100644
--- a/src/sbbs3/prntfile.cpp
+++ b/src/sbbs3/prntfile.cpp
@@ -22,6 +22,7 @@
 #include "sbbs.h"
 #include "utf8.h"
 #include "petdefs.h"
+#include "mode7defs.h"
 
 #ifndef PRINTFILE_MAX_LINE_LEN
 #define PRINTFILE_MAX_LINE_LEN (8 * 1024)
@@ -54,6 +55,8 @@ bool sbbs_t::printfile(const char* fname, int mode, int org_cols, JSObject* obj)
 			mode |= P_NOPAUSE;
 		} else if (stricmp(p, ".seq") == 0) {
 			mode |= P_PETSCII;
+		} else if (stricmp(p, ".m7") == 0) {
+			mode |= P_MODE7;
 		} else if (stricmp(p, ".utf8") == 0) {
 			mode |= P_UTF8;
 		}
@@ -283,6 +286,8 @@ bool sbbs_t::menu(const char *code, int mode, JSObject* obj)
 				break;
 			if ((term & PETSCII) && menu_exists(code, "seq", path))
 				break;
+			if ((term & MODE7) && menu_exists(code, "m7", path))
+				break;
 			if (term & NO_EXASCII) {
 				next = "asc";
 				last = "msg";
diff --git a/src/sbbs3/sbbs.h b/src/sbbs3/sbbs.h
index 5723daed8b..411a1cfe29 100644
--- a/src/sbbs3/sbbs.h
+++ b/src/sbbs3/sbbs.h
@@ -981,6 +981,7 @@ public:
 	bool	saveline(void);
 	bool	restoreline(void);
 	int		petscii_to_ansibbs(unsigned char);
+	int		mode7_to_ansibbs(unsigned char);
 	size_t	print_utf8_as_cp437(const char*, size_t);
 	int		attr(int);				/* Change text color/attributes */
 	void	ctrl_a(char);			/* Performs Ctrl-Ax attribute changes */
@@ -1431,6 +1432,7 @@ extern "C" {
 
 	/* con_out.cpp */
 	unsigned char		cp437_to_petscii(unsigned char);
+	unsigned char		cp437_to_mode7(unsigned char);
 
 	/* xtrn.cpp */
 	bool				native_executable(scfg_t*, const char* cmdline, int mode);
diff --git a/src/sbbs3/sbbs_ini.c b/src/sbbs3/sbbs_ini.c
index 6ba8123a24..a550ea306b 100644
--- a/src/sbbs3/sbbs_ini.c
+++ b/src/sbbs3/sbbs_ini.c
@@ -448,6 +448,9 @@ void sbbs_read_ini(
 		bbs->pet80_port
 		    = iniGetShortInt(list, section, "Pet80Port", 128);
 
+		bbs->mode7_port
+			= iniGetShortInt(list, section, "Mode7Port", 5050);
+
 		bbs->ssh_port
 		    = iniGetShortInt(list, section, "SSHPort", 22);
 		bbs->ssh_connect_timeout
@@ -936,6 +939,9 @@ bool sbbs_write_ini(
 			if (!iniSetUInt16(lp, section, "Pet80Port", bbs->pet80_port, &style))
 				break;
 
+			if (!iniSetUInt16(lp, section, "Mode7Port", bbs->mode7_port, &style))
+				break;
+
 			if (strListCmp(bbs->ssh_interfaces, global->interfaces) == 0)
 				iniRemoveValue(lp, section, "SSHInterface");
 			else if (!iniSetStringList(lp, section, "SSHInterface", ",", bbs->ssh_interfaces, &style))
diff --git a/src/sbbs3/sbbsdefs.h b/src/sbbs3/sbbsdefs.h
index b4c285ca30..d1ac52f537 100644
--- a/src/sbbs3/sbbsdefs.h
+++ b/src/sbbs3/sbbsdefs.h
@@ -568,7 +568,7 @@ typedef enum {                      /* Values for xtrn_t.event				*/
 #define COLDKEYS    (1 << 18)     /* No hot-keys							*/
 #define EXTDESC     (1 << 19)     /* Extended file descriptions			*/
 #define AUTOHANG    (1 << 20)     /* Auto-hang-up after transfer			*/
-#define WIP_UNUSED  (1 << 21)     /* Supports WIP terminal emulation		*/
+#define MODE7		(1 << 21)     /* BBC Micro Mode 7 terminal support	*/
 #define AUTOLOGON   (1 << 22)     /* AutoLogon via IP						*/
 #define HTML_UNUSED (1 << 23)     /* Using Zuul/HTML terminal				*/
 #define NOPAUSESPIN (1 << 24)     /* No spinning cursor at pause prompt	*/
@@ -579,9 +579,10 @@ typedef enum {                      /* Values for xtrn_t.event				*/
 #define MOUSE       (1U << 31)    /* Mouse supported terminal				*/
 
 #define TERM_FLAGS      (ANSI | COLOR | RIP | SWAP_DELETE | ICE_COLOR | MOUSE | CHARSET_FLAGS)
-#define CHARSET_FLAGS   (NO_EXASCII | PETSCII | UTF8)
+#define CHARSET_FLAGS   (NO_EXASCII | PETSCII | UTF8 | MODE7)
 #define CHARSET_ASCII   NO_EXASCII  // US-ASCII
 #define CHARSET_PETSCII PETSCII     // CBM-ASCII
+#define CHARSET_MODE7	MODE7		// MODE 7
 #define CHARSET_UTF8    UTF8
 #define CHARSET_CP437   0
 
@@ -694,6 +695,7 @@ typedef enum {                      /* Values for xtrn_t.event				*/
 #define P_REMOTE    (1 << 18)     /* Only print when online == ON_REMOTE		*/
 #define P_INDENT    (1 << 19)     /* Indent lines to current cursor column	*/
 #define P_ATCODES   (1 << 20)     /* Trusted @-codes in formatted string		*/
+#define P_MODE7		(1 << 21)     /* Message is native Mode 7					*/
 
 /* Bits in 'mode' for listfiles             */
 #define FL_ULTIME   (1 << 0)      /* List files by upload time                */
diff --git a/src/sbbs3/scfg/scfgsrvr.c b/src/sbbs3/scfg/scfgsrvr.c
index 4e870ce2c3..b52ce44848 100644
--- a/src/sbbs3/scfg/scfgsrvr.c
+++ b/src/sbbs3/scfg/scfgsrvr.c
@@ -557,6 +557,8 @@ static void termsrvr_cfg(void)
 		snprintf(opt[i++], MAX_OPLN, "%-30s%s", "40 Column PETSCII Support", startup.pet40_port ? str : strDisabled );
 		snprintf(str, sizeof str, "Port %u", startup.pet80_port);
 		snprintf(opt[i++], MAX_OPLN, "%-30s%s", "80 Column PETSCII Support", startup.pet80_port  ? str : strDisabled);
+		snprintf(str, sizeof str, "Port %u", startup.mode7_port);
+		snprintf(opt[i++], MAX_OPLN, "%-30s%s", "BBC Mode 7 Support", startup.mode7_port  ? str : strDisabled);
 		snprintf(opt[i++], MAX_OPLN, "%-30s%s", "DOS Program Support", startup.options & BBS_OPT_NO_DOS ? "No" : "Yes");
 		snprintf(opt[i++], MAX_OPLN, "%-30s%s", "Max Concurrent Connections", maximum(startup.max_concurrent_connections));
 		snprintf(opt[i++], MAX_OPLN, "%-30s%s", "Max Login Inactivity", vduration(startup.max_login_inactivity));
@@ -621,9 +623,14 @@ static void termsrvr_cfg(void)
 					startup.pet80_port = atoi(str);
 				break;
 			case 8:
-				startup.options ^= BBS_OPT_NO_DOS;
+				SAFEPRINTF(str, "%u", startup.mode7_port);
+				if (uifc.input(WIN_MID | WIN_SAV, 0, 0, "BBC Mode 7 Port", str, 5, K_NUMBER | K_EDIT) > 0)
+					startup.mode7_port = atoi(str);
 				break;
 			case 9:
+				startup.options ^= BBS_OPT_NO_DOS;
+				break;
+			case 10:
 				SAFECOPY(str, maximum(startup.max_concurrent_connections));
 				if (uifc.input(WIN_MID | WIN_SAV, 0, 0, "Maximum Concurrent (Unauthenticated) Connections", str, 10, K_EDIT) > 0)
 					startup.max_concurrent_connections = atoi(str);
@@ -637,7 +644,7 @@ static void termsrvr_cfg(void)
 		"`Maximum User Inactivity` setting in `System->Advanced Options`.\n" \
 		"Normally, if enabled, this socket inactivity duration should be `longer`\n" \
 		"than the `Maximum User Inactivity` setting in `System->Advanced Options`.\n"
-			case 10:
+			case 11:
 				uifc.helpbuf =
 					"`Maximum Socket Inactivity at Login:`\n"
 					"\n"
@@ -651,7 +658,7 @@ static void termsrvr_cfg(void)
 				if (uifc.input(WIN_MID | WIN_SAV, 0, 0, "Maximum Socket Inactivity at Login", str, 10, K_EDIT) > 0)
 					startup.max_login_inactivity = (uint16_t)parse_duration(str);
 				break;
-			case 11:
+			case 12:
 				uifc.helpbuf =
 					"`Maximum Socket Inactivity at New User Registration:`\n"
 					"\n"
@@ -665,7 +672,7 @@ static void termsrvr_cfg(void)
 				if (uifc.input(WIN_MID | WIN_SAV, 0, 0, "Maximum Socket Inactivity at New User Registration", str, 10, K_EDIT) > 0)
 					startup.max_newuser_inactivity = (uint16_t)parse_duration(str);
 				break;
-			case 12:
+			case 13:
 				uifc.helpbuf =
 					"`Maximum Socket Inactivity during User Session:`\n"
 					"\n"
@@ -681,34 +688,34 @@ static void termsrvr_cfg(void)
 				if (uifc.input(WIN_MID | WIN_SAV, 0, 0, "Maximum Socket Inactivity during User Session", str, 10, K_EDIT) > 0)
 					startup.max_session_inactivity = (uint16_t)parse_duration(str);
 				break;
-			case 13:
+			case 14:
 				SAFEPRINTF(str, "%u", startup.outbuf_drain_timeout);
 				if (uifc.input(WIN_MID | WIN_SAV, 0, 0, "Output Buffer Drain Timeout (milliseconds)", str, 5, K_NUMBER | K_EDIT) > 0)
 					startup.outbuf_drain_timeout = atoi(str);
 				break;
-			case 14:
+			case 15:
 				startup.options ^= BBS_OPT_NO_EVENTS;
 				break;
-			case 15:
+			case 16:
 				if (startup.options & BBS_OPT_NO_EVENTS)
 					break;
 				startup.options ^= BBS_OPT_NO_QWK_EVENTS;
 				break;
-			case 16:
+			case 17:
 				if (startup.options & BBS_OPT_NO_EVENTS)
 					break;
 				uifc.list(WIN_MID | WIN_SAV, 0, 0, 0, &startup.event_log_level, 0, "Event Log Level", iniLogLevelStringList());
 				break;
-			case 17:
+			case 18:
 				startup.options ^= BBS_OPT_NO_HOST_LOOKUP;
 				break;
-			case 18:
+			case 19:
 				getar("Terminal Server Login", startup.login_ars);
 				break;
-			case 19:
+			case 20:
 				js_startup_cfg(&startup.js);
 				break;
-			case 20:
+			case 21:
 				login_attempt_cfg(&startup.login_attempt);
 				break;
 			default:
diff --git a/src/sbbs3/startup.h b/src/sbbs3/startup.h
index bdefe555fa..58d70b865b 100644
--- a/src/sbbs3/startup.h
+++ b/src/sbbs3/startup.h
@@ -129,6 +129,7 @@ typedef struct {
 	uint16_t rlogin_port;
 	uint16_t pet40_port;            // 40-column PETSCII terminal server
 	uint16_t pet80_port;            // 80-column PETSCII terminal server
+	uint16_t mode7_port;			// 40-column MODE7 terminal server
 	uint16_t ssh_port;
 	uint16_t ssh_connect_timeout;
 	int ssh_error_level;
diff --git a/src/sbbs3/text.h b/src/sbbs3/text.h
index b7097f503e..548c2785b4 100644
--- a/src/sbbs3/text.h
+++ b/src/sbbs3/text.h
@@ -954,6 +954,8 @@ enum text {
 	,QWKTagLineFmt
 	,QWKControlCommand
 	,QWKBadControlCommand
+	,Mode7TerminalDetected
+	,Mode7TerminalQ
 
 	,TOTAL_TEXT
 };
diff --git a/src/sbbs3/text_defaults.c b/src/sbbs3/text_defaults.c
index 6c568a1ec9..4929b181ae 100644
--- a/src/sbbs3/text_defaults.c
+++ b/src/sbbs3/text_defaults.c
@@ -1507,4 +1507,6 @@ const char * const text_defaults[TOTAL_TEXT]={
 		"\x25\x73\x0d\x0a" // 936 QWKControlCommand
 	,"\x55\x6e\x72\x65\x63\x6f\x67\x6e\x69\x7a\x65\x64\x20\x43\x6f\x6e\x74\x72\x6f\x6c\x20\x43\x6f\x6d\x6d\x61\x6e\x64\x21\x01\x6e\x0d"
 		"\x0a" // 937 QWKBadControlCommand
+	,"\x4d\x6f\x64\x65\x20\x37\x20\x74\x65\x72\x6d\x69\x6e\x61\x6c\x20\x64\x65\x74\x65\x63\x74\x65\x64" // 938 Mode7TerminalDetected
+	,"\x41\x72\x65\x20\x79\x6f\x75\x20\x75\x73\x69\x6e\x67\x20\x61\x20\x4d\x6f\x64\x65\x20\x37\x20\x74\x65\x72\x6d\x69\x6e\x61\x6c" // 939 Mode7TerminalQ
 };
diff --git a/src/sbbs3/text_id.c b/src/sbbs3/text_id.c
index abf977c944..213aaf1491 100644
--- a/src/sbbs3/text_id.c
+++ b/src/sbbs3/text_id.c
@@ -938,4 +938,6 @@ const char* const text_id[]={
 	,"QWKTagLineFmt"
 	,"QWKControlCommand"
 	,"QWKBadControlCommand"
+	,"Mode7TerminalDetected"
+	,"Mode7TerminalQ"
 };
diff --git a/src/sbbs3/userdat.c b/src/sbbs3/userdat.c
index 5d28ebe2d6..9049296eb1 100644
--- a/src/sbbs3/userdat.c
+++ b/src/sbbs3/userdat.c
@@ -2177,6 +2177,12 @@ static bool ar_exp(scfg_t* cfg, uchar **ptrptr, user_t* user, client_t* client)
 				else
 					result = !not;
 				break;
+			case AR_MODE7:
+				if (user == NULL || (user->misc & CHARSET_FLAGS) != CHARSET_MODE7)
+					result = not;
+				else
+					result = !not;
+				break;
 			case AR_ASCII:
 				if (user == NULL || (user->misc & CHARSET_FLAGS) != CHARSET_ASCII)
 					result = not;
diff --git a/src/sbbs3/useredit.cpp b/src/sbbs3/useredit.cpp
index eddc505f0c..77d9366f6c 100644
--- a/src/sbbs3/useredit.cpp
+++ b/src/sbbs3/useredit.cpp
@@ -880,7 +880,7 @@ void sbbs_t::user_config(user_t* user)
 			case 'T':
 				if (yesno(text[AutoTerminalQ])) {
 					user->misc |= AUTOTERM;
-					user->misc &= ~(ANSI | RIP | PETSCII | UTF8);
+					user->misc &= ~(ANSI | RIP | PETSCII | UTF8 | MODE7);
 					if (user == &useron)
 						user->misc |= autoterm;
 				}
diff --git a/src/sbbs3/xtrn.cpp b/src/sbbs3/xtrn.cpp
index 69b8ef8cd4..152d43e00f 100644
--- a/src/sbbs3/xtrn.cpp
+++ b/src/sbbs3/xtrn.cpp
@@ -214,6 +214,13 @@ static void petscii_convert(BYTE* buf, uint len)
 	}
 }
 
+static void mode7_convert(BYTE* buf, uint len)
+{
+	for (uint i = 0; i < len; i++) {
+		buf[i] = cp437_to_mode7(buf[i]);
+	}
+}
+
 static BYTE* cp437_to_utf8(BYTE* input, size_t& len, BYTE* outbuf, size_t maxlen)
 {
 	size_t outlen = 0;
@@ -1961,6 +1968,8 @@ int sbbs_t::external(const char* cmdline, int mode, const char* startup_dir)
 				}
 				if (term_supports(PETSCII))
 					petscii_convert(bp, output_len);
+				else if (term_supports(MODE7))
+					mode7_convert(bp, output_len);
 				else if (term_supports(UTF8))
 					bp = cp437_to_utf8(bp, output_len, utf8_buf, sizeof utf8_buf);
 			}
-- 
GitLab