diff --git a/exec/load/cga_defs.js b/exec/load/cga_defs.js
index 2a59c873760f85cb6d7a40f4a04db85a1f546fdf..7abe627e00e4efd11e5947f4bc821e9ead6d70f8 100644
--- a/exec/load/cga_defs.js
+++ b/exec/load/cga_defs.js
@@ -44,10 +44,15 @@ var colors = [
 	'YELLOW',
 	'WHITE'
 	];
-							    /* background colors */
-var   ANSI_NORMAL	=0x100;		/* special value for ansi() */
+var   FG_UNKNOWN	=0x100;
 var   BG_BLACK		=0x200;		/* special value for ansi() */
 var   BG_HIGH		=0x400;		/* not an ANSI.SYS compatible attribute */
+var   REVERSED		=0x800;
+var   UNDERLINE		=0x1000;
+var   CONCEALED		=0x2000;
+var   BG_UNKNOWN	=0x4000;
+var   ANSI_NORMAL	=(FG_UNKNOWN | BG_UNKNOWN);
+							    /* background colors */
 var   BG_BLUE		=(BLUE<<4);
 var   BG_GREEN		=(GREEN<<4);
 var   BG_CYAN		=(CYAN<<4);
diff --git a/exec/xbimage.js b/exec/xbimage.js
index 5f0b5df64c0cf7c3dba86be12576093f981c053c..4cc6f97646e3970733e463ba6e65c9514abd6635 100644
--- a/exec/xbimage.js
+++ b/exec/xbimage.js
@@ -67,7 +67,7 @@ function convert_from_bmp(filename, charheight, fg_color, bg_color, palette, inv
 
 function show(filename, xpos, ypos, fg_color, bg_color, palette, delay, cleanup)
 {
-	if((console.term_supports()&(USER_ANSI|USER_NO_EXASCII|USER_UTF8|USER_ICE_COLOR))
+	if((console.term_supports()&(USER_ANSI|USER_NO_EXASCII|USER_UTF8))
 		!= USER_ANSI)
 		return false;
 		
diff --git a/src/sbbs3/ansi_parser.cpp b/src/sbbs3/ansi_parser.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..42ed0eebd0b5f0e654c781db4b6efd6d7d419604
--- /dev/null
+++ b/src/sbbs3/ansi_parser.cpp
@@ -0,0 +1,193 @@
+#include "ansi_parser.h"
+
+#include <stdio.h>
+enum ansiState
+ANSI_Parser::parse(unsigned char ch)
+{
+	switch (state) {
+		case ansiState_none:
+			if (ch == '\x1b') {
+				state = ansiState_esc;
+				ansi_sequence += ch;
+			}
+			break;
+		case ansiState_esc:
+			ansi_sequence += ch;
+			if (ch == '[') {
+				state = ansiState_csi;
+				ansi_params = "";
+			}
+			else if (ch == '_' || ch == 'P' || ch == '^' || ch == ']') {
+				state = ansiState_string;
+				ansi_was_string = true;
+			}
+			else if (ch == 'X') {
+				state = ansiState_sos;
+				ansi_was_string = true;
+			}
+			else if (ch >= ' ' && ch <= '/') {
+				ansi_ibs += ch;
+				state = ansiState_intermediate;
+			}
+			else if (ch >= '0' && ch <= '~') {
+				state = ansiState_final;
+				ansi_was_cc = true;
+				ansi_final_byte = ch;
+			}
+			else {
+				state = ansiState_broken;
+			}
+			break;
+		case ansiState_csi:
+			ansi_sequence += ch;
+			if (ch >= '0' && ch <= '?') {
+				if (ansi_params == "" && ch >= '<' && ch <= '?')
+					ansi_was_private = true;
+				ansi_params += ch;
+			}
+			else if (ch >= ' ' && ch <= '/') {
+				ansi_ibs += ch;
+				state = ansiState_intermediate;
+			}
+			else if (ch >= '@' && ch <= '~') {
+				state = ansiState_final;
+				ansi_final_byte = ch;
+			}
+			else {
+				state = ansiState_broken;
+			}
+			break;
+		case ansiState_intermediate:
+			ansi_sequence += ch;
+			if (ch >= ' ' && ch <= '/') {
+				ansi_ibs += ch;
+				state = ansiState_intermediate;
+			}
+			else if (ch >= '@' && ch <= '~') {
+				state = ansiState_final;
+				ansi_final_byte = ch;
+			}
+			else {
+				state = ansiState_broken;
+			}
+			break;
+		case ansiState_string: // APS, DCS, PM, or OSC
+			ansi_sequence += ch;
+			if (ch == '\x1b')
+				state = ansiState_esc;
+			else if (!((ch >= '\b' && ch <= '\r') || (ch >= ' ' && ch <= '~')))
+				state = ansiState_broken;
+			break;
+		case ansiState_sos: // SOS
+			ansi_sequence += ch;
+			if (ch == '\x1b')
+				state = ansiState_sos_esc;
+			break;
+		case ansiState_sos_esc: // ESC inside SOS
+			ansi_sequence += ch;
+			if (ch == '\\')
+				state = ansiState_esc;
+			else if (ch == 'X')
+				state = ansiState_broken;
+			else
+				state = ansiState_sos;
+			break;
+		case ansiState_broken:
+			// Stay in broken state.
+			break;
+		case ansiState_final:
+			// Stay in final state.
+			break;
+	}
+	return state;
+}
+
+enum ansiState
+ANSI_Parser::current_state()
+{
+	return state;
+}
+
+void
+ANSI_Parser::reset()
+{
+	ansi_params.clear();
+	ansi_ibs.clear();
+	ansi_sequence.clear();
+	state = ansiState_none;
+	ansi_final_byte = 0;
+	ansi_was_cc = false;
+	ansi_was_string = false;
+	ansi_was_private = false;
+}
+
+unsigned
+ANSI_Parser::count_params()
+{
+	std::string tp = ansi_params;
+	unsigned ret = 1;
+
+	try {
+		for (;;) {
+			size_t sc = tp.find(";");
+			if (sc == std::string::npos)
+				return ret;
+			ret++;
+			tp.erase(0, sc + 1);
+		}
+	}
+	catch (...) {
+		return 0;
+	}
+}
+
+unsigned
+ANSI_Parser::get_pval(unsigned pnum, unsigned dflt)
+{
+	try {
+		if (ansi_params == "")
+			return dflt;
+		unsigned p = 0;
+		std::string tp = ansi_params;
+		switch (tp.at(0)) {
+			case '<':
+			case '=':
+			case '>':
+			case '?':
+				tp.erase(0, 1);
+				break;
+		}
+		while (p < pnum) {
+			size_t sc = tp.find(";");
+			if (sc == std::string::npos)
+				return dflt;
+			tp.erase(0, sc + 1);
+			p++;
+		}
+		size_t sc = tp.find(";");
+		if (sc != std::string::npos)
+			tp.erase(sc);
+		sc = tp.find(":");
+		if (sc != std::string::npos)
+			tp.erase(sc);
+		sc = tp.find("<");
+		if (sc != std::string::npos)
+			tp.erase(sc);
+		sc = tp.find("=");
+		if (sc != std::string::npos)
+			tp.erase(sc);
+		sc = tp.find(">");
+		if (sc != std::string::npos)
+			tp.erase(sc);
+		sc = tp.find("?");
+		if (sc != std::string::npos)
+			tp.erase(sc);
+		if (tp == "")
+			return dflt;
+		return std::stoul(tp);
+	}
+	catch (...) {
+		return dflt;
+	}
+}
+
diff --git a/src/sbbs3/ansi_parser.h b/src/sbbs3/ansi_parser.h
new file mode 100644
index 0000000000000000000000000000000000000000..a038c9b99206f9ab835e82cd6e0ee62f624a9fb3
--- /dev/null
+++ b/src/sbbs3/ansi_parser.h
@@ -0,0 +1,38 @@
+#ifndef ANSI_PARSE_H
+#define ANSI_PARSE_H
+
+#include <string>
+
+enum ansiState {
+	 ansiState_none         // No sequence
+	,ansiState_esc          // Escape
+	,ansiState_csi          // CSI
+	,ansiState_intermediate // Intermediate byte
+	,ansiState_final        // Final byte
+	,ansiState_string       // APS, DCS, PM, or OSC
+	,ansiState_sos          // SOS
+	,ansiState_sos_esc      // ESC inside SOS
+	,ansiState_broken	// Invalid ANSI
+};
+
+class ANSI_Parser {
+public:
+	enum ansiState parse(unsigned char ch);
+	enum ansiState current_state();
+	void reset();
+	unsigned count_params();
+	unsigned get_pval(unsigned pnum, unsigned dflt);
+
+	std::string ansi_sequence{""};
+	std::string ansi_params{""};
+	std::string ansi_ibs{""};
+	char ansi_final_byte{0};
+	bool ansi_was_cc{false};
+	bool ansi_was_string{false};
+	bool ansi_was_private{false};
+
+private:
+	enum ansiState state{ansiState_none}; // track ANSI escape seq output
+};
+
+#endif
diff --git a/src/sbbs3/ansi_terminal.cpp b/src/sbbs3/ansi_terminal.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..ba151d8383e0e2dd9d20edd12f48457935608e5d
--- /dev/null
+++ b/src/sbbs3/ansi_terminal.cpp
@@ -0,0 +1,1168 @@
+#include "ansi_terminal.h"
+
+enum ansi_mouse_mode {
+	ANSI_MOUSE_X10  = 9,
+	ANSI_MOUSE_NORM = 1000,
+	ANSI_MOUSE_BTN  = 1002,
+	ANSI_MOUSE_ANY  = 1003,
+	ANSI_MOUSE_EXT  = 1006
+};
+
+// Was ansi()
+const char *ANSI_Terminal::attrstr(unsigned atr)
+{
+	switch (atr) {
+
+		/* Special case */
+		case ANSI_NORMAL:
+			return "\x1b[0m";
+		case BLINK:
+		case BG_BRIGHT:
+			return "\x1b[5m";
+
+		/* Foreground */
+		case HIGH:
+			return "\x1b[1m";
+		case BLACK:
+			return "\x1b[30m";
+		case RED:
+			return "\x1b[31m";
+		case GREEN:
+			return "\x1b[32m";
+		case BROWN:
+			return "\x1b[33m";
+		case BLUE:
+			return "\x1b[34m";
+		case MAGENTA:
+			return "\x1b[35m";
+		case CYAN:
+			return "\x1b[36m";
+		case LIGHTGRAY:
+			return "\x1b[37m";
+
+		/* Background */
+		case BG_BLACK:
+			return "\x1b[40m";
+		case BG_RED:
+			return "\x1b[41m";
+		case BG_GREEN:
+			return "\x1b[42m";
+		case BG_BROWN:
+			return "\x1b[43m";
+		case BG_BLUE:
+			return "\x1b[44m";
+		case BG_MAGENTA:
+			return "\x1b[45m";
+		case BG_CYAN:
+			return "\x1b[46m";
+		case BG_LIGHTGRAY:
+			return "\x1b[47m";
+	}
+
+	return "-Invalid use of ansi()-";
+}
+
+static uint32_t
+popcnt(const uint32_t val)
+{
+	uint32_t i = val;
+
+	// Clang optimizes this to popcnt on my system.
+	i = i - ((i >> 1) & 0x55555555);
+	i = (i & 0x33333333) + ((i >> 2) & 0x33333333);
+	i = (i + (i >> 4)) & 0x0F0F0F0F;
+	i *= 0x01010101;
+	return  i >> 24;
+}
+
+// Was ansi() and ansi_attr()
+char* ANSI_Terminal::attrstr(unsigned atr, unsigned curatr, char* str, size_t strsz)
+{
+	if (!supports(COLOR)) {  /* eliminate colors if terminal doesn't support them */
+		if (atr & LIGHTGRAY)       /* if any foreground bits set, set all */
+			atr |= LIGHTGRAY;
+		if (atr & BG_LIGHTGRAY)  /* if any background bits set, set all */
+			atr |= BG_LIGHTGRAY;
+		if ((atr & LIGHTGRAY) && (atr & BG_LIGHTGRAY))
+			atr &= ~LIGHTGRAY;  /* if background is solid, foreground is black */
+		if (!atr)
+			atr |= LIGHTGRAY;   /* don't allow black on black */
+	}
+
+	if (atr & FG_UNKNOWN)
+		atr &= ~0x07;
+	if (atr & BG_UNKNOWN)
+		atr &= ~0x70;
+	if (curatr & FG_UNKNOWN)
+		curatr &= ~0x07;
+	if (curatr & BG_UNKNOWN)
+		curatr &= ~0x70;
+
+	size_t lastret;
+	if (supports(ICE_COLOR)) {
+		switch (atr & (BG_BRIGHT | BLINK)) {
+			case BG_BRIGHT:
+			case BLINK:
+				atr ^= BLINK;
+				break;
+		}
+		switch (curatr & (BG_BRIGHT | BLINK)) {
+			case BG_BRIGHT:
+			case BLINK:
+				curatr ^= BLINK;
+				break;
+		}
+	}
+
+	if (curatr == atr) { /* text hasn't changed. no sequence needed */
+		*str = 0;
+		return str;
+	}
+
+	lastret = strlcpy(str, "\033[", strsz);
+	uint32_t changed_mask = (curatr ^ atr) & (HIGH | BLINK | REVERSED | UNDERLINE | CONCEALED);
+	// TODO: CSI 0 m does *NOT* set 
+	if (changed_mask) {
+		uint32_t set = popcnt(changed_mask & atr);
+		uint32_t clear = popcnt(changed_mask & ~atr);
+		uint32_t set_only_weight = set * 2 + 8;
+		uint32_t set_clear_weight = set * 2 + clear * 3;
+
+		if (atr & 0x70)
+			set_only_weight += 3;
+		if (!(atr & FG_UNKNOWN))
+			set_only_weight += 3;
+		if (!(atr & BG_UNKNOWN))
+			set_only_weight += 3;
+
+		if ((atr & (BG_UNKNOWN | 0x70)) != (curatr & (BG_UNKNOWN |0x70)))
+			set_clear_weight += 3;
+		if ((atr & (FG_UNKNOWN | 0x07)) != (curatr & (FG_UNKNOWN | 0x07)))
+			set_clear_weight += 3;
+
+		if (set_only_weight < set_clear_weight) {
+			lastret = strlcat(str, "0;", strsz);
+			curatr &= ~0x77;
+			curatr |= ANSI_NORMAL;
+		}
+	}
+	if (atr & HIGH) {                     /* special attributes */
+		if (!(curatr & HIGH))
+			lastret = strlcat(str, "1;", strsz);
+	}
+	else {
+		if (curatr & HIGH)
+			lastret = strlcat(str, "22;", strsz);
+	}
+	if (atr & BLINK) {
+		if (!(curatr & BLINK))
+			lastret = strlcat(str, "5;", strsz);
+	}
+	else {
+		if (curatr & BLINK)
+			lastret = strlcat(str, "25;", strsz);
+	}
+	if (atr & REVERSED) {
+		if (!(curatr & REVERSED))
+			lastret = strlcat(str, "7;", strsz);
+	}
+	else {
+		if (curatr & REVERSED)
+			lastret = strlcat(str, "27;", strsz);
+	}
+	if (atr & UNDERLINE) {
+		if (!(curatr & UNDERLINE))
+			lastret = strlcat(str, "4;", strsz);
+	}
+	else {
+		if (curatr & UNDERLINE)
+			lastret = strlcat(str, "24;", strsz);
+	}
+	if (atr & CONCEALED) {
+		if (!(curatr & CONCEALED))
+			lastret = strlcat(str, "8;", strsz);
+	}
+	else {
+		if (curatr & CONCEALED)
+			lastret = strlcat(str, "28;", strsz);
+	}
+	if ((atr & FG_UNKNOWN) && !(curatr & FG_UNKNOWN)) {
+		lastret = strlcat(str, "39;", strsz);
+		curatr &= ~0x07;
+		curatr |= FG_UNKNOWN;
+	}
+	if ((atr & (FG_UNKNOWN | 0x07)) != (curatr & (FG_UNKNOWN | 0x07))) {
+		switch (atr & 0x07) {
+			case BLACK:
+				lastret = strlcat(str, "30;", strsz);
+				break;
+			case RED:
+				lastret = strlcat(str, "31;", strsz);
+				break;
+			case GREEN:
+				lastret = strlcat(str, "32;", strsz);
+				break;
+			case BROWN:
+				lastret = strlcat(str, "33;", strsz);
+				break;
+			case BLUE:
+				lastret = strlcat(str, "34;", strsz);
+				break;
+			case MAGENTA:
+				lastret = strlcat(str, "35;", strsz);
+				break;
+			case CYAN:
+				lastret = strlcat(str, "36;", strsz);
+				break;
+			case LIGHTGRAY:
+				lastret = strlcat(str, "37;", strsz);
+				break;
+		}
+	}
+	if ((atr & BG_UNKNOWN) && !(curatr & BG_UNKNOWN)) {
+		lastret = strlcat(str, "39;", strsz);
+		curatr &= ~0x70;
+		curatr |= BG_UNKNOWN;
+	}
+	if ((atr & (BG_UNKNOWN | 0x70)) != (curatr & (BG_UNKNOWN | 0x70))) {
+		switch (atr & 0x70) {
+			/* The BG_BLACK macro is 0x200, so isn't in the mask */
+			case 0 /* BG_BLACK */:
+				lastret = strlcat(str, "40;", strsz);
+				break;
+			case BG_RED:
+				lastret = strlcat(str, "41;", strsz);
+				break;
+			case BG_GREEN:
+				lastret = strlcat(str, "42;", strsz);
+				break;
+			case BG_BROWN:
+				lastret = strlcat(str, "43;", strsz);
+				break;
+			case BG_BLUE:
+				lastret = strlcat(str, "44;", strsz);
+				break;
+			case BG_MAGENTA:
+				lastret = strlcat(str, "45;", strsz);
+				break;
+			case BG_CYAN:
+				lastret = strlcat(str, "46;", strsz);
+				break;
+			case BG_LIGHTGRAY:
+				lastret = strlcat(str, "47;", strsz);
+				break;
+		}
+	}
+	if (lastret == 2) {  /* Convert <ESC>[ to blank */
+		lastret = 0;
+		if (strsz > 0) {
+			*str = 0;
+		}
+	}
+	else {
+		// Replace ; with m
+		if (strsz > (lastret)) {
+			str[lastret - 1] = 'm';
+			str[lastret] = 0;
+		}
+		lastret++;
+	}
+
+	if (lastret >= strsz) {
+		sbbs->lprintf(LOG_ERR, "ANSI sequence attr %02X to %02X, strsz %zu too small", curatr, atr, strsz);
+		if (strsz)
+			str[0] = 0;
+	}
+
+	return str;
+}
+
+#define TIMEOUT_ANSI_GETXY  5   // Seconds
+bool ANSI_Terminal::getdims()
+{
+	if (sbbs->sys_status & SS_USERON
+	    && (sbbs->useron.rows == TERM_ROWS_AUTO || sbbs->useron.cols == TERM_COLS_AUTO)
+	    && sbbs->online == ON_REMOTE) {                                 /* Remote */
+		sbbs->term_out("\x1b[s\x1b[255B\x1b[255C\x1b[6n\x1b[u");
+		return sbbs->inkey(K_ANSI_CPR, TIMEOUT_ANSI_GETXY * 1000) == 0;
+	}
+	return false;
+}
+
+bool ANSI_Terminal::getxy(unsigned* x, unsigned* y)
+{
+	size_t rsp = 0;
+	int    ch;
+	char   str[128];
+	enum { state_escape, state_open, state_y, state_x } state = state_escape;
+
+	if (x != NULL)
+		*x = 0;
+	if (y != NULL)
+		*y = 0;
+
+	sbbs->term_out("\x1b[6n");  /* Request cursor position */
+
+	time_t start = time(NULL);
+	sbbs->sys_status &= ~SS_ABORT;
+	while (sbbs->online && !(sbbs->sys_status & SS_ABORT) && rsp < sizeof(str) - 1) {
+		if ((ch = sbbs->incom(1000)) != NOINP) {
+			str[rsp++] = ch;
+			if (ch == ESC && state == state_escape) {
+				state = state_open;
+				start = time(NULL);
+			}
+			else if (ch == '[' && state == state_open) {
+				state = state_y;
+				start = time(NULL);
+			}
+			else if (IS_DIGIT(ch) && state == state_y) {
+				if (y != NULL) {
+					(*y) *= 10;
+					(*y) += (ch & 0xf);
+				}
+				start = time(NULL);
+			}
+			else if (ch == ';' && state == state_y) {
+				state = state_x;
+				start = time(NULL);
+			}
+			else if (IS_DIGIT(ch) && state == state_x) {
+				if (x != NULL) {
+					(*x) *= 10;
+					(*x) += (ch & 0xf);
+				}
+				start = time(NULL);
+			}
+			else if (ch == 'R' && state == state_x)
+				break;
+			else {
+				str[rsp] = '\0';
+#ifdef _DEBUG
+				char dbg[128];
+				c_escape_str(str, dbg, sizeof(dbg), /* Ctrl-only? */ true);
+				sbbs->lprintf(LOG_DEBUG, "Unexpected ansi_getxy response: '%s'", dbg);
+#endif
+				sbbs->ungetkeys(str, /* insert */ false);
+				rsp = 0;
+				state = state_escape;
+			}
+		}
+		if (time(NULL) - start > TIMEOUT_ANSI_GETXY) {
+			sbbs->lprintf(LOG_NOTICE, "!TIMEOUT in ansi_getxy");
+			return false;
+		}
+	}
+
+	return true;
+}
+
+bool ANSI_Terminal::gotoxy(unsigned x, unsigned y)
+{
+	if (x == 0)
+		x = 1;
+	if (y == 0)
+		y = 1;
+	sbbs->term_printf("\x1b[%d;%dH", y, x);
+	return true;
+}
+
+// Was ansi_save
+bool ANSI_Terminal::save_cursor_pos()
+{
+	sbbs->term_out("\x1b[s");
+	return true;
+}
+
+// Was ansi_restore
+bool ANSI_Terminal::restore_cursor_pos()
+{
+	sbbs->term_out("\x1b[u");
+	return true;
+}
+
+void ANSI_Terminal::clearscreen()
+{
+	clear_hotspots();
+	sbbs->term_out("\x1b[2J\x1b[H");    /* clear screen, home cursor */
+	lastcrcol = 0;
+}
+
+void ANSI_Terminal::cleartoeos()
+{
+	sbbs->term_out("\x1b[J");
+}
+
+void ANSI_Terminal::cleartoeol()
+{
+	sbbs->term_out("\x1b[K");
+}
+
+void ANSI_Terminal::cursor_home()
+{
+	sbbs->term_out("\x1b[H");
+}
+
+void ANSI_Terminal::cursor_up(unsigned count = 1)
+{
+	if (count == 0)
+		return;
+	if (count > 1)
+		sbbs->term_printf("\x1b[%dA", count);
+	else
+		sbbs->term_out("\x1b[A");
+}
+
+void ANSI_Terminal::cursor_down(unsigned count = 1)
+{
+	if (count == 0)
+		return;
+	if (count > 1)
+		sbbs->term_printf("\x1b[%dB", count);
+	else
+		sbbs->term_out("\x1b[B");
+}
+
+void ANSI_Terminal::cursor_right(unsigned count = 1)
+{
+	if (count == 0)
+		return;
+	if (count > 1)
+		sbbs->term_printf("\x1b[%dC", count);
+	else
+		sbbs->term_out("\x1b[C");
+}
+
+void ANSI_Terminal::cursor_left(unsigned count = 1) {
+	if (count == 0)
+		return;
+	if (count < 4)
+		sbbs->term_printf("%.*s", count, "\b\b\b");
+	else
+		sbbs->term_printf("\x1b[%dD", count);
+}
+
+void ANSI_Terminal::set_output_rate(enum output_rate speed) {
+	unsigned int val = speed;
+	switch (val) {
+		case 0:     val = 0; break;
+		case 600:   val = 2; break;
+		case 1200:  val = 3; break;
+		case 2400:  val = 4; break;
+		case 4800:  val = 5; break;
+		case 9600:  val = 6; break;
+		case 19200: val = 7; break;
+		case 38400: val = 8; break;
+		case 57600: val = 9; break;
+		case 76800: val = 10; break;
+		default:
+			if (val <= 300)
+				val = 1;
+			else if (val > 76800)
+				val = 11;
+			break;
+	}
+	sbbs->term_printf("\x1b[;%u*r", val);
+	cur_output_rate = speed;
+}
+
+const char* ANSI_Terminal::type() {return "ANSI";}
+
+static void ansi_mouse(sbbs_t *sbbs, enum ansi_mouse_mode mode, bool enable)
+{
+	char str[32] = "";
+	SAFEPRINTF2(str, "\x1b[?%u%c", mode, enable ? 'h' : 'l');
+	sbbs->term_out(str);
+}
+
+void ANSI_Terminal::set_mouse(unsigned flags) {
+	if ((!supports(MOUSE)) && (mouse_mode != MOUSE_MODE_OFF))
+		flags = MOUSE_MODE_OFF;
+	if (supports(MOUSE) || flags == MOUSE_MODE_OFF) {
+		unsigned mode = mouse_mode & ~flags;
+		if (mode & MOUSE_MODE_X10)
+			ansi_mouse(sbbs, ANSI_MOUSE_X10, false);
+		if (mode & MOUSE_MODE_NORM)
+			ansi_mouse(sbbs, ANSI_MOUSE_NORM, false);
+		if (mode & MOUSE_MODE_BTN)
+			ansi_mouse(sbbs, ANSI_MOUSE_BTN, false);
+		if (mode & MOUSE_MODE_ANY)
+			ansi_mouse(sbbs, ANSI_MOUSE_ANY, false);
+		if (mode & MOUSE_MODE_EXT)
+			ansi_mouse(sbbs, ANSI_MOUSE_EXT, false);
+
+		mode = flags & ~mouse_mode;
+		if (mode & MOUSE_MODE_X10)
+			ansi_mouse(sbbs, ANSI_MOUSE_X10, true);
+		if (mode & MOUSE_MODE_NORM)
+			ansi_mouse(sbbs, ANSI_MOUSE_NORM, true);
+		if (mode & MOUSE_MODE_BTN)
+			ansi_mouse(sbbs, ANSI_MOUSE_BTN, true);
+		if (mode & MOUSE_MODE_ANY)
+			ansi_mouse(sbbs, ANSI_MOUSE_ANY, true);
+		if (mode & MOUSE_MODE_EXT)
+			ansi_mouse(sbbs, ANSI_MOUSE_EXT, true);
+
+		if (mouse_mode != flags) {
+			mouse_mode = flags;
+		}
+	}
+}
+
+void ANSI_Terminal::handle_control_code() {
+	if (ansiParser.ansi_ibs == "") {
+		switch (ansiParser.ansi_final_byte) {
+			case 'E':	// NEL - Next Line
+				set_column();
+				inc_row();
+				break;
+			case 'M':	// RI - Reverse Line Feed
+				dec_row();
+				break;
+			case 'c':	// RIS - Reset to Initial State.. homes
+				set_column();
+				set_row();
+				break;
+		}
+	}
+}
+
+void ANSI_Terminal::set_color(int c, bool bg)
+{
+	if (curatr & REVERSED)
+		bg = !bg;
+	if (bg) {
+		curatr &= ~(BG_BLACK | BG_UNKNOWN | 0x70);
+		if (c == FG_UNKNOWN)
+			curatr |= BG_UNKNOWN;
+		else
+			curatr |= c << 4;
+	}
+	else {
+		curatr &= ~(FG_UNKNOWN | 0x07);
+		curatr |= c;
+	}
+}
+
+void ANSI_Terminal::handle_SGR_sequence() {
+	unsigned cnt = ansiParser.count_params();
+	unsigned pval;
+
+	for (unsigned i = 0; i < cnt; i++) {
+		pval = ansiParser.get_pval(i, 0);
+		switch (pval) {
+			case 0:
+				curatr = ANSI_NORMAL;
+				break;
+			case 1:
+				curatr |= HIGH;
+				break;
+			case 2:
+				curatr &= ~HIGH;
+				break;
+			case 4:
+				curatr |= UNDERLINE;
+				break;
+			case 5:
+			case 6:
+				if (flags_ & ICE_COLOR)
+					curatr |= BG_BRIGHT;
+				else
+					curatr |= BLINK;
+				break;
+			case 7:
+				if (!(curatr & REVERSED))
+					curatr = REVERSED | (curatr & ~0x77) | ((curatr & 0x70) >> 4) | ((curatr & 0x07) << 4);
+				break;
+			case 8:
+				curatr |= CONCEALED;
+				break;
+			case 22:
+				curatr &= ~HIGH;
+				break;
+			case 24:
+				curatr &= ~UNDERLINE;
+				break;
+			case 25:
+				// Turn both off...
+				curatr &= ~(BG_BRIGHT|BLINK);
+				break;
+			case 27:
+				if (curatr & REVERSED)
+					curatr = (curatr & ~(REVERSED | 0x77)) | ((curatr & 0x70) >> 4) | ((curatr & 0x07) << 4);
+				break;
+			case 28:
+				curatr &= ~CONCEALED;
+				break;
+			case 30:
+				set_color(BLACK, false);
+				break;
+			case 31:
+				set_color(RED, false);
+				break;
+			case 32:
+				set_color(GREEN, false);
+				break;
+			case 33:
+				set_color(BROWN, false);
+				break;
+			case 34:
+				set_color(BLUE, false);
+				break;
+			case 35:
+				set_color(MAGENTA, false);
+				break;
+			case 36:
+				set_color(CYAN, false);
+				break;
+			case 37:
+				set_color(LIGHTGRAY, false);
+				break;
+			case 39:
+				set_color(FG_UNKNOWN, false);
+				break;
+			case 40:
+				set_color(BLACK, true);
+				break;
+			case 41:
+				set_color(RED, true);
+				break;
+			case 42:
+				set_color(GREEN, true);
+				break;
+			case 43:
+				set_color(BROWN, true);
+				break;
+			case 44:
+				set_color(BLUE, true);
+				break;
+			case 45:
+				set_color(MAGENTA, true);
+				break;
+			case 46:
+				set_color(CYAN, true);
+				break;
+			case 47:
+				set_color(LIGHTGRAY, true);
+				break;
+			case 49:
+				set_color(FG_UNKNOWN, true);
+				break;
+		}
+	}
+}
+
+void ANSI_Terminal::handle_control_sequence() {
+	unsigned pval;
+
+	if (ansiParser.ansi_was_private) {
+		// TODO: Track things like origin mode, auto wrap, etc.
+	}
+	else {
+		if (ansiParser.ansi_ibs == "") {
+			switch (ansiParser.ansi_final_byte) {
+				case 'A':	// Cursor up
+				case 'F':	// Cursor Preceding Line
+				case 'k':	// Line Position Backward
+					// Single parameter, default 1
+					pval = ansiParser.get_pval(0, 1);
+					if (pval > row)
+						pval = row;
+					dec_row(pval);
+					break;
+				case 'B':	// Cursor Down
+				case 'E':	// Cursor Next Line
+				case 'e':	// Line position Forward
+					// Single parameter, default 1
+					pval = ansiParser.get_pval(0, 1);
+					if (pval >= (rows - row))
+						pval = rows - row - 1;
+					inc_row(pval);
+					break;
+				case 'C':	// Cursor Right
+				case 'a':	// Cursor Position Forward
+					// Single parameter, default 1
+					pval = ansiParser.get_pval(0, 1);
+					if (pval >= (cols - column))
+						pval = cols - column - 1;
+					inc_column(pval);
+					break;
+				case 'D':	// Cursor Left
+				case 'j':	// Character Position Backward
+					// Single parameter, default 1
+					pval = ansiParser.get_pval(0, 1);
+					if (pval > column)
+						pval = column;
+					dec_column(pval);
+					break;
+				case 'G':	// Cursor Character Absolute
+				case '`':	// Character Position Absolute
+					pval = ansiParser.get_pval(0, 1);
+					if (pval > cols)
+						pval = cols;
+					if (pval == 0)
+						pval = 1;
+					set_column(pval - 1);
+					break;
+				case 'H':	// Cursor Position
+				case 'f':	// Character and Line Position
+					pval = ansiParser.get_pval(0, 1);
+					if (pval > rows)
+						pval = rows;
+					if (pval == 0)
+						pval = 1;
+					set_row(pval - 1);
+					pval = ansiParser.get_pval(1, 1);
+					if (pval > cols)
+						pval = cols;
+					if (pval == 0)
+						pval = 1;
+					set_column(pval - 1);
+					break;
+				case 'I':	// Cursor Forward Tabulation
+					// TODO
+					break;
+				case 'J':	// Clear screen
+					// TODO: ANSI does not move cursor, ANSI-BBS does.
+					set_row();
+					set_column();
+					break;
+				case 'Y':	// Line tab
+					// TODO: Track tabs
+					break;
+				case 'Z':	// Back tab
+					// TODO: Track tabs
+					break;
+				case 'b':	// Repeat
+					// TODO: Can't repeat ESC
+					break;
+				case 'd':	// Line Position Abolute
+					pval = ansiParser.get_pval(0, 1);
+					if (pval > rows)
+						pval = rows;
+					if (pval == 0)
+						pval = 1;
+					set_row(pval - 1);
+					break;
+				case 'm':	// Set Graphic Rendidtion
+					handle_SGR_sequence();
+					break;
+				case 'r':	// Set Top and Bottom Margins
+					// TODO
+					break;
+				case 's':	// Save Current Position (also set left/right margins)
+					saved_row = row;
+					saved_column = column;
+					break;
+				case 'u':	// Restore Cursor Position
+					set_row(saved_row);
+					set_column(saved_column);
+					break;
+				case 'z':	// Invoke Macro
+					// TODO
+					break;
+				case 'N':	// "ANSI" Music
+				case '|':	// SyncTERM Music
+					// TODO
+					break;
+			}
+		}
+	}
+}
+
+bool ANSI_Terminal::parse_output(char ich) {
+	unsigned char ch = static_cast<unsigned char>(ich);
+
+	if (utf8_increment(ch))
+		return true;
+
+	switch (ansiParser.parse(ch)) {
+		case ansiState_final:
+			if (ansiParser.ansi_was_cc)
+				handle_control_code();
+			else if (!ansiParser.ansi_was_string)
+				handle_control_sequence();
+			ansiParser.reset();
+			return true;
+		case ansiState_broken:
+			sbbs->lprintf(LOG_WARNING, "Sent broken ANSI sequence '%s'", ansiParser.ansi_sequence.c_str());
+			ansiParser.reset();
+			return true;
+		case ansiState_none:
+			break;
+		default:
+			return true;
+	}
+
+	/* Track cursor position locally */
+	switch (ch) {
+		case '\a':  // 7
+			/* Non-printing */
+			break;
+		case 8:  // BS
+			dec_column();
+			break;
+		case 9:	// TAB
+			if (column < (cols - 1)) {
+				inc_column();
+				while ((column < (cols - 1)) && (column % 8))
+					inc_column();
+			}
+			break;
+		case 10: // LF
+			inc_row();
+			break;
+		case 12: // FF
+			set_row();
+			set_column();
+			break;
+		case 13: // CR
+			lastcrcol = column;
+			if (sbbs->console & CON_CR_CLREOL)
+				cleartoeol();
+			set_column();
+			break;
+		default:
+			if ((charset() == CHARSET_UTF8) && (ch < 32)) {
+				// Assume all other UTF-8 control characters do nothing.
+				// TODO: Some might (ie: VT) but we can't really know
+				//       what they'll do.
+				break;
+			}
+			// Assume other CP427 control characters print a glyph.
+			inc_column();
+			break;
+	}
+
+	return true;
+}
+
+bool ANSI_Terminal::stuff_unhandled(char &ch, ANSI_Parser& ansi)
+{
+	if (ansi.ansi_sequence == "")
+		return false;
+	ch = ESC;
+	std::string remain = ansi.ansi_sequence.substr(1);
+	for (auto uch = remain.crbegin(); uch != remain.crend(); uch++) {
+		sbbs->ungetkey(*uch, true);
+	}
+	return true;
+}
+
+bool ANSI_Terminal::stuff_str(char& ch, const char *str, bool skipctlcheck)
+{
+	const char *end = str;
+	bool ret = false;
+	if (!skipctlcheck) {
+		if (str[0] < 32) {
+			ret = true;
+			end = &str[1];
+		}
+	}
+	for (const char *p = strchr(str, 0) - 1; p >= end; p--) {
+		sbbs->ungetkey(*p, true);
+	}
+	return ret;
+}
+
+bool ANSI_Terminal::handle_left_press(unsigned x, unsigned y, char& ch, bool& retval)
+{
+	list_node_t* node = find_hotspot(x, y);
+	if (node != NULL) {
+		struct mouse_hotspot* spot = (struct mouse_hotspot*)node->data;
+#ifdef _DEBUG
+		{
+			char dbg[128];
+			c_escape_str(spot->cmd, dbg, sizeof(dbg), /* Ctrl-only? */ true);
+			sbbs->lprintf(LOG_DEBUG, "Stuffing hot spot command into keybuf: '%s'", dbg);
+		}
+#endif
+		if (sbbs->pause_inside && !pause_hotspot) {
+			// Abort the pause then send command
+			ch = TERM_KEY_ABORT;
+			stuff_str(ch, spot->cmd);
+			retval = true;
+			return true;
+		}
+		else {
+			retval = stuff_str(ch, spot->cmd);
+			return retval;
+		}
+	}
+	if (sbbs->pause_inside && y == rows - 1) {
+		ch = '\r';
+		retval = true;
+		return true;
+	}
+	return false;
+}
+
+bool ANSI_Terminal::handle_non_SGR_mouse_sequence(char& ch, ANSI_Parser& ansi)
+{
+	int button = sbbs->kbincom(100);
+	if (button == NOINP) {
+		sbbs->lprintf(LOG_DEBUG, "Timeout waiting for mouse button value");
+		return false;
+	}
+	if (button < ' ') {
+		sbbs->lprintf(LOG_DEBUG, "Unexpected mouse-button (0x%02X) tracking char: 0x%02X < ' '"
+			, button, ch);
+		return false;
+	}
+	button -= ' ';
+	bool move{false};
+	bool release{false};
+	if (button == 3) {
+		release = true;
+		button &= ~0x20;
+	}
+	if (button & 0x20) {
+		move = true;
+		button &= ~0x20;
+	}
+	if (button >= 128)
+		button -= 121;
+	else if (button >= 64)
+		button -= 61;
+	if (button >= 11) {
+		sbbs->lprintf(LOG_DEBUG, "Unexpected mouse-button (0x%02X) decoded", button);
+		return false;
+	}
+	int inch = sbbs->kbincom(100);
+	if (inch == NOINP) {
+		sbbs->lprintf(LOG_DEBUG, "Timeout waiting for mouse X value");
+		return false;
+	}
+	if (inch < '!') {
+		sbbs->lprintf(LOG_DEBUG, "Unexpected mouse-button (0x%02X) tracking char: 0x%02X < '!'"
+			, button, ch);
+		return false;
+	}
+	int x = ch - '!';
+	inch = sbbs->kbincom(100);
+	if (inch == NOINP) {
+		sbbs->lprintf(LOG_DEBUG, "Timeout waiting for mouse Y value");
+		return false;
+	}
+	if (ch < '!') {
+		sbbs->lprintf(LOG_DEBUG, "Unexpected mouse-button (0x%02X) tracking char: 0x%02X < '!'"
+			, button, ch);
+		return false;
+	}
+	int y = ch - '!';
+	sbbs->lprintf(LOG_DEBUG, "X10 Mouse button-%s (0x%02X) reported at: %u x %u", release ? "release" : (move ? "move" : "press"), button, x, y);
+	if (button == 0 && release == false && move == false) {
+		bool retval{false};
+		if (handle_left_press(x, y, ch, retval))
+			return retval;
+	}
+	else if (button == 3 && release == false && move == false) {
+		ch = TERM_KEY_UP;
+		return true;
+	}
+	else if (button == 4 && release == false && move == false) {
+		ch = TERM_KEY_DOWN;
+		return true;
+	}
+	if ((sbbs->console & CON_MOUSE_CLK_PASSTHRU) && (!release))
+		return stuff_unhandled(ch, ansi);
+	if ((sbbs->console & CON_MOUSE_REL_PASSTHRU) && release)
+		return stuff_unhandled(ch, ansi);
+	if (button == 2) {
+		ch = TERM_KEY_ABORT;
+		return true;
+	}
+	return false;
+}
+
+bool ANSI_Terminal::handle_SGR_mouse_sequence(char& ch, ANSI_Parser& ansi, bool release)
+{
+	if (ansi.count_params() != 3) {
+		sbbs->lprintf(LOG_DEBUG, "Invalid SGR mouse report sequence: '%s'", ansi.ansi_params.c_str());
+		return false;
+	}
+	unsigned button = ansi.get_pval(0, 256);
+	bool move{false};
+	if (button & 0x20) {
+		move = true;
+		button &= ~0x20;
+	}
+	if (button >= 128)
+		button -= 121;
+	else if (button >= 64)
+		button -= 61;
+	if (button >= 11) {
+		sbbs->lprintf(LOG_DEBUG, "Unexpected SGR mouse-button (0x%02X) decoded", button);
+		return false;
+	}
+	unsigned x = ansi.get_pval(1, 0);
+	unsigned y = ansi.get_pval(2, 0);
+	if (x < 1 || y < 1) {
+		sbbs->lprintf(LOG_DEBUG, "Invalid SGR mouse report position: '%s'", ansi.ansi_params.c_str());
+		return false;
+	}
+	x--;
+	y--;
+	if (button == 0 && release == true && move == false) {
+		bool retval{false};
+		if (handle_left_press(x, y, ch, retval))
+			return retval;
+	}
+	else if (button == 3 && release == false && move == false) {
+		ch = TERM_KEY_UP;
+		return true;
+	}
+	else if (button == 4 && release == false && move == false) {
+		ch = TERM_KEY_DOWN;
+		return true;
+	}
+	if ((sbbs->console & CON_MOUSE_CLK_PASSTHRU) && (!release))
+		return stuff_unhandled(ch, ansi);
+	if ((sbbs->console & CON_MOUSE_REL_PASSTHRU) && release)
+		return stuff_unhandled(ch, ansi);
+	if (button == 2) {
+		ch = TERM_KEY_ABORT;
+		return true;
+	}
+	return false;
+}
+
+bool ANSI_Terminal::parse_input_sequence(char& ch, int mode) {
+	if (ch == ESC) {
+		ANSI_Parser ansi{};
+		ansi.parse(ESC);
+		bool done = false;
+		unsigned timeouts = 0;
+		while (!done) {
+			int rc = sbbs->kbincom(100);
+			if (rc == NOINP) {	// Timed out
+				if (++timeouts >= 30)
+					break;
+			}
+			switch(ansi.parse(rc)) {
+				case ansiState_final:
+					done = 1;
+					break;
+				case ansiState_broken:
+					sbbs->lprintf(LOG_DEBUG, "Invalid ANSI sequence: '\\e%s'", &(ansi.ansi_sequence.c_str()[1]));
+					done = 1;
+					break;
+				default:
+					break;
+			}
+		}
+		switch (ansi.current_state()) {
+			case ansiState_final:
+				if ((!ansi.ansi_was_private)
+				    && ansi.ansi_ibs == ""
+				    && (!ansi.ansi_was_cc)) {
+					if (ansi.ansi_params == "") {
+						switch (ansi.ansi_final_byte) {
+							case 'A':
+								ch = TERM_KEY_UP;
+								return true;
+							case 'B':
+								ch = TERM_KEY_DOWN;
+								return true;
+							case 'C':
+								ch = TERM_KEY_RIGHT;
+								return true;
+							case 'D':
+								ch = TERM_KEY_LEFT;
+								return true;
+							case 'H':
+								ch = TERM_KEY_HOME;
+								return true;
+							case 'V':
+								ch = TERM_KEY_PAGEUP;
+								return true;
+							case 'U':
+								ch = TERM_KEY_PAGEDN;
+								return true;
+							case 'F':
+								ch = TERM_KEY_END;
+								return true;
+							case 'K':
+								ch = TERM_KEY_END;
+								return true;
+							case 'M':
+								return handle_non_SGR_mouse_sequence(ch, ansi);
+							case '@':
+								ch = TERM_KEY_INSERT;
+								return true;
+						}
+					}
+					else if (ansi.ansi_final_byte == '~' && ansi.count_params() == 1) {
+						switch (ansi.get_pval(0, 0)) {
+							case 1:
+								ch = TERM_KEY_HOME;
+								return true;
+							case 2:
+								ch = TERM_KEY_INSERT;
+								return true;
+							case 3:
+								ch = TERM_KEY_DELETE;
+								return true;
+							case 4:
+								ch = TERM_KEY_END;
+								return true;
+							case 5:
+								ch = TERM_KEY_PAGEUP;
+								return true;
+							case 6:
+								ch = TERM_KEY_PAGEDN;
+								return true;
+						}
+					}
+					else if (ansi.ansi_final_byte == 'R') {
+						if (mode & K_ANSI_CPR) {  /* auto-detect rows */
+							unsigned x = ansi.get_pval(1, 1);
+							unsigned y = ansi.get_pval(0, 1);
+							sbbs->lprintf(LOG_DEBUG, "received ANSI cursor position report: %ux%u"
+								, x, y);
+							/* Sanity check the coordinates in the response: */
+							bool update{false};
+							if (sbbs->useron.cols == TERM_COLS_AUTO && x >= TERM_COLS_MIN && x <= TERM_COLS_MAX) {
+								cols = x;
+								update = true;
+							}
+							if (sbbs->useron.rows == TERM_ROWS_AUTO && y >= TERM_ROWS_MIN && y <= TERM_ROWS_MAX) {
+								rows = y;
+								update = true;
+							}
+							if (update)
+								sbbs->update_nodeterm();
+						}
+						// TODO: This was suppressed before the terminal-abstration
+						//       branch.  I can't think of a good reason to keep doing
+						//       that though, and feel it should be more like the rest.
+						ch = 0;
+						return true;
+					}
+				}
+				else if (ansi.ansi_was_private
+				    && ansi.ansi_ibs == ""
+				    && (!ansi.ansi_was_cc)) {
+					if (ansi.ansi_sequence[2] == '<') {
+						switch (ansi.ansi_final_byte) {
+							case 'm':
+								return handle_SGR_mouse_sequence(ch, ansi, true);
+							case 'M':
+								return handle_SGR_mouse_sequence(ch, ansi, false);
+						}
+					}
+				}
+				// Fall through
+			default:
+				return stuff_unhandled(ch, ansi);
+		}
+	}
+	return false;
+}
+
+struct mouse_hotspot* ANSI_Terminal::add_hotspot(struct mouse_hotspot* spot) {return nullptr;}
+
+bool ANSI_Terminal::can_highlight() { return true; }
+bool ANSI_Terminal::can_move() { return true; }
+bool ANSI_Terminal::can_mouse()  { return flags_ & MOUSE; }
+bool ANSI_Terminal::is_monochrome() { return !(flags_ & COLOR); }
diff --git a/src/sbbs3/ansi_terminal.h b/src/sbbs3/ansi_terminal.h
new file mode 100644
index 0000000000000000000000000000000000000000..fb215b37a590385dea61c2ad54a90b3267da395d
--- /dev/null
+++ b/src/sbbs3/ansi_terminal.h
@@ -0,0 +1,60 @@
+#ifndef ANSI_TERMINAL_H
+#define ANSI_TERMINAL_H
+
+#include <string>
+#include "ansi_parser.h"
+#include "terminal.h"
+
+class ANSI_Terminal : public Terminal {
+public:
+	ANSI_Terminal() = delete;
+	using Terminal::Terminal;
+
+	// Was ansi()
+	virtual const char *attrstr(unsigned atr);
+	// Was ansi() and ansi_attr()
+	virtual char* attrstr(unsigned atr, unsigned curatr, char* str, size_t strsz);
+	virtual bool getdims();
+	virtual bool getxy(unsigned* x, unsigned* y);
+	virtual bool gotoxy(unsigned x, unsigned y);
+	// Was ansi_save
+	virtual bool save_cursor_pos();
+	// Was ansi_restore
+	virtual bool restore_cursor_pos();
+	virtual void clearscreen();
+	virtual void cleartoeos();
+	virtual void cleartoeol();
+	virtual void cursor_home();
+	virtual void cursor_up(unsigned count);
+	virtual void cursor_down(unsigned count);
+	virtual void cursor_right(unsigned count);
+	virtual void cursor_left(unsigned count);
+	virtual void set_output_rate(enum output_rate speed);
+	virtual const char* type();
+	virtual void set_mouse(unsigned mode);
+	virtual bool parse_output(char ch);
+	// Needs to handle C0 and C1
+	virtual bool parse_input_sequence(char& ch, int mode);
+	virtual struct mouse_hotspot* add_hotspot(struct mouse_hotspot* spot);
+	virtual bool can_highlight();
+	virtual bool can_move();
+	virtual bool can_mouse();
+	virtual bool is_monochrome();
+
+private:
+	void handle_control_code();
+	void handle_control_sequence();
+	void handle_SGR_sequence();
+	bool stuff_unhandled(char &ch, ANSI_Parser& ansi);
+	bool stuff_str(char& ch, const char *str, bool skipctlcheck = false);
+	bool handle_non_SGR_mouse_sequence(char& ch, ANSI_Parser& ansi);
+	bool handle_SGR_mouse_sequence(char& ch, ANSI_Parser& ansi, bool release);
+	bool handle_left_press(unsigned x, unsigned y, char& ch, bool& retval);
+	void set_color(int c, bool bg);
+
+	ANSI_Parser ansiParser{};
+	unsigned saved_row{0};
+	unsigned saved_column{0};
+};
+
+#endif
diff --git a/src/sbbs3/ansiterm.cpp b/src/sbbs3/ansiterm.cpp
deleted file mode 100644
index a61d71e9163e88266361409adf5f4206a18ca706..0000000000000000000000000000000000000000
--- a/src/sbbs3/ansiterm.cpp
+++ /dev/null
@@ -1,311 +0,0 @@
-/* Synchronet ANSI terminal functions */
-
-/****************************************************************************
- * @format.tab-size 4		(Plain Text/Source Code File Header)			*
- * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
- *																			*
- * Copyright Rob Swindell - http://www.synchro.net/copyright.html			*
- *																			*
- * This program is free software; you can redistribute it and/or			*
- * modify it under the terms of the GNU General Public License				*
- * as published by the Free Software Foundation; either version 2			*
- * of the License, or (at your option) any later version.					*
- * See the GNU General Public License for more details: gpl.txt or			*
- * http://www.fsf.org/copyleft/gpl.html										*
- *																			*
- * For Synchronet coding style and modification guidelines, see				*
- * http://www.synchro.net/source.html										*
- *																			*
- * Note: If this box doesn't appear square, then you need to fix your tabs.	*
- ****************************************************************************/
-
-#include "sbbs.h"
-
-#define TIMEOUT_ANSI_GETXY  5   // Seconds
-
-/****************************************************************************/
-/* Returns the ANSI code to obtain the value of atr. Mixed attributes		*/
-/* high intensity colors, or background/foreground combinations don't work. */
-/* A call to attr() is more appropriate, being it is intelligent			*/
-/****************************************************************************/
-const char *sbbs_t::ansi(int atr)
-{
-
-	switch (atr) {
-
-		/* Special case */
-		case ANSI_NORMAL:
-			return "\x1b[0m";
-		case BLINK:
-		case BG_BRIGHT:
-			return "\x1b[5m";
-
-		/* Foreground */
-		case HIGH:
-			return "\x1b[1m";
-		case BLACK:
-			return "\x1b[30m";
-		case RED:
-			return "\x1b[31m";
-		case GREEN:
-			return "\x1b[32m";
-		case BROWN:
-			return "\x1b[33m";
-		case BLUE:
-			return "\x1b[34m";
-		case MAGENTA:
-			return "\x1b[35m";
-		case CYAN:
-			return "\x1b[36m";
-		case LIGHTGRAY:
-			return "\x1b[37m";
-
-		/* Background */
-		case BG_BLACK:
-			return "\x1b[40m";
-		case BG_RED:
-			return "\x1b[41m";
-		case BG_GREEN:
-			return "\x1b[42m";
-		case BG_BROWN:
-			return "\x1b[43m";
-		case BG_BLUE:
-			return "\x1b[44m";
-		case BG_MAGENTA:
-			return "\x1b[45m";
-		case BG_CYAN:
-			return "\x1b[46m";
-		case BG_LIGHTGRAY:
-			return "\x1b[47m";
-	}
-
-	return "-Invalid use of ansi()-";
-}
-
-/* insure str is at least 14 bytes in size! */
-extern "C" char* ansi_attr(int atr, int curatr, char* str, bool color)
-{
-	if (!color) {  /* eliminate colors if terminal doesn't support them */
-		if (atr & LIGHTGRAY)       /* if any foreground bits set, set all */
-			atr |= LIGHTGRAY;
-		if (atr & BG_LIGHTGRAY)  /* if any background bits set, set all */
-			atr |= BG_LIGHTGRAY;
-		if ((atr & LIGHTGRAY) && (atr & BG_LIGHTGRAY))
-			atr &= ~LIGHTGRAY;  /* if background is solid, foreground is black */
-		if (!atr)
-			atr |= LIGHTGRAY;   /* don't allow black on black */
-	}
-	if (curatr == atr) { /* text hasn't changed. no sequence needed */
-		*str = 0;
-		return str;
-	}
-
-	strcpy(str, "\033[");
-	if ((!(atr & HIGH) && curatr & HIGH) || (!(atr & BLINK) && curatr & BLINK)
-	    || atr == LIGHTGRAY) {
-		strcat(str, "0;");
-		curatr = LIGHTGRAY;
-	}
-	if (atr & BLINK) {                     /* special attributes */
-		if (!(curatr & BLINK))
-			strcat(str, "5;");
-	}
-	if (atr & HIGH) {
-		if (!(curatr & HIGH))
-			strcat(str, "1;");
-	}
-	if ((atr & 0x07) != (curatr & 0x07)) {
-		switch (atr & 0x07) {
-			case BLACK:
-				strcat(str, "30;");
-				break;
-			case RED:
-				strcat(str, "31;");
-				break;
-			case GREEN:
-				strcat(str, "32;");
-				break;
-			case BROWN:
-				strcat(str, "33;");
-				break;
-			case BLUE:
-				strcat(str, "34;");
-				break;
-			case MAGENTA:
-				strcat(str, "35;");
-				break;
-			case CYAN:
-				strcat(str, "36;");
-				break;
-			case LIGHTGRAY:
-				strcat(str, "37;");
-				break;
-		}
-	}
-	if ((atr & 0x70) != (curatr & 0x70)) {
-		switch (atr & 0x70) {
-			/* The BG_BLACK macro is 0x200, so isn't in the mask */
-			case 0 /* BG_BLACK */:
-				strcat(str, "40;");
-				break;
-			case BG_RED:
-				strcat(str, "41;");
-				break;
-			case BG_GREEN:
-				strcat(str, "42;");
-				break;
-			case BG_BROWN:
-				strcat(str, "43;");
-				break;
-			case BG_BLUE:
-				strcat(str, "44;");
-				break;
-			case BG_MAGENTA:
-				strcat(str, "45;");
-				break;
-			case BG_CYAN:
-				strcat(str, "46;");
-				break;
-			case BG_LIGHTGRAY:
-				strcat(str, "47;");
-				break;
-		}
-	}
-	if (strlen(str) == 2)  /* Convert <ESC>[ to blank */
-		*str = 0;
-	else
-		str[strlen(str) - 1] = 'm';
-	return str;
-}
-
-char* sbbs_t::ansi(int atr, int curatr, char* str)
-{
-	long term = term_supports();
-	if (term & ICE_COLOR) {
-		switch (atr & (BG_BRIGHT | BLINK)) {
-			case BG_BRIGHT:
-			case BLINK:
-				atr ^= BLINK;
-				break;
-		}
-	}
-	return ::ansi_attr(atr, curatr, str, (term & COLOR) ? TRUE:FALSE);
-}
-
-bool sbbs_t::ansi_getdims()
-{
-	if (sys_status & SS_USERON && useron.misc & ANSI
-	    && (useron.rows == TERM_ROWS_AUTO || useron.cols == TERM_COLS_AUTO)
-	    && online == ON_REMOTE) {                                 /* Remote */
-		putcom("\x1b[s\x1b[255B\x1b[255C\x1b[6n\x1b[u");
-		return inkey(K_ANSI_CPR, TIMEOUT_ANSI_GETXY * 1000) == 0;
-	}
-	return false;
-}
-
-bool sbbs_t::ansi_getxy(int* x, int* y)
-{
-	size_t rsp = 0;
-	int    ch;
-	char   str[128];
-	enum { state_escape, state_open, state_y, state_x } state = state_escape;
-
-	if (x != NULL)
-		*x = 0;
-	if (y != NULL)
-		*y = 0;
-
-	putcom("\x1b[6n");  /* Request cursor position */
-
-	time_t start = time(NULL);
-	sys_status &= ~SS_ABORT;
-	while (online && !(sys_status & SS_ABORT) && rsp < sizeof(str) - 1) {
-		if ((ch = incom(1000)) != NOINP) {
-			str[rsp++] = ch;
-			if (ch == ESC && state == state_escape) {
-				state = state_open;
-				start = time(NULL);
-			}
-			else if (ch == '[' && state == state_open) {
-				state = state_y;
-				start = time(NULL);
-			}
-			else if (IS_DIGIT(ch) && state == state_y) {
-				if (y != NULL) {
-					(*y) *= 10;
-					(*y) += (ch & 0xf);
-				}
-				start = time(NULL);
-			}
-			else if (ch == ';' && state == state_y) {
-				state = state_x;
-				start = time(NULL);
-			}
-			else if (IS_DIGIT(ch) && state == state_x) {
-				if (x != NULL) {
-					(*x) *= 10;
-					(*x) += (ch & 0xf);
-				}
-				start = time(NULL);
-			}
-			else if (ch == 'R' && state == state_x)
-				break;
-			else {
-				str[rsp] = '\0';
-#ifdef _DEBUG
-				char dbg[128];
-				c_escape_str(str, dbg, sizeof(dbg), /* Ctrl-only? */ true);
-				lprintf(LOG_DEBUG, "Unexpected ansi_getxy response: '%s'", dbg);
-#endif
-				ungetkeys(str, /* insert */ false);
-				rsp = 0;
-				state = state_escape;
-			}
-		}
-		if (time(NULL) - start > TIMEOUT_ANSI_GETXY) {
-			lprintf(LOG_NOTICE, "!TIMEOUT in ansi_getxy");
-			return false;
-		}
-	}
-
-	return true;
-}
-
-bool sbbs_t::ansi_gotoxy(int x, int y)
-{
-	if (term_supports(ANSI)) {
-		comprintf("\x1b[%d;%dH", y, x);
-		if (x > 0)
-			column = x - 1;
-		if (y > 0)
-			row = y - 1;
-		lncntr = 0;
-		return true;
-	}
-	return false;
-}
-
-bool sbbs_t::ansi_save(void)
-{
-	if (term_supports(ANSI)) {
-		putcom("\x1b[s");
-		return true;
-	}
-	return false;
-}
-
-bool sbbs_t::ansi_restore(void)
-{
-	if (term_supports(ANSI)) {
-		putcom("\x1b[u");
-		return true;
-	}
-	return false;
-}
-
-int sbbs_t::ansi_mouse(enum ansi_mouse_mode mode, bool enable)
-{
-	char str[32] = "";
-	SAFEPRINTF2(str, "\x1b[?%u%c", mode, enable ? 'h' : 'l');
-	return putcom(str);
-}
diff --git a/src/sbbs3/answer.cpp b/src/sbbs3/answer.cpp
index 2c0e2921b3ffc0d37c23498349bd229259306fea..939e529c8ab8a10f16fda22fb989a1eb9db60832 100644
--- a/src/sbbs3/answer.cpp
+++ b/src/sbbs3/answer.cpp
@@ -429,8 +429,8 @@ bool sbbs_t::answer()
 						if (((startup->options & (BBS_OPT_ALLOW_SFTP | BBS_OPT_SSH_ANYAUTH)) == BBS_OPT_ALLOW_SFTP) && tnamelen == 4 && strncmp(tname, "sftp", 4) == 0) {
 							if (useron.number) {
 								activate_ssh = init_sftp(cid);
-								cols = 0;
-								rows = 0;
+								term->cols = 0;
+								term->rows = 0;
 								SAFECOPY(terminal, "sftp");
 								mouse_mode = MOUSE_MODE_OFF;
 								autoterm = 0;
@@ -509,12 +509,12 @@ bool sbbs_t::answer()
 		}
 
 		if (cryptStatusOK(cryptGetAttribute(ssh_session, CRYPT_SESSINFO_SSH_CHANNEL_WIDTH, &l)) && l > 0) {
-			cols = l;
-			lprintf(LOG_DEBUG, "%04d SSH [%s] height %d", client_socket, client.addr, cols);
+			term->cols = l;
+			lprintf(LOG_DEBUG, "%04d SSH [%s] height %d", client_socket, client.addr, term->cols);
 		}
 		if (cryptStatusOK(cryptGetAttribute(ssh_session, CRYPT_SESSINFO_SSH_CHANNEL_HEIGHT, &l)) && l > 0) {
-			rows = l;
-			lprintf(LOG_DEBUG, "%04d SSH [%s] height %d", client_socket, client.addr, rows);
+			term->rows = l;
+			lprintf(LOG_DEBUG, "%04d SSH [%s] height %d", client_socket, client.addr, term->rows);
 		}
 		l = 0;
 		if (cryptStatusOK(cryptGetAttributeString(ssh_session, CRYPT_SESSINFO_SSH_CHANNEL_TERMINAL, terminal, &l)) && l > 0) {
@@ -546,15 +546,57 @@ bool sbbs_t::answer()
 
 	/* Detect terminal type */
 	if (!term_output_disabled) {
-		mswait(200);    // Allow some time for Telnet negotiation
+		// Grab telnet terminal if negotiated already
+		if (!(telnet_mode & TELNET_MODE_OFF)) {
+			if (autoterm == 0) {
+				unsigned loops = 0;
+				// Wait up to 2s more for telnet term type
+				// TODO: Any way to detect if the remote send a zero-length type?
+				lprintf(LOG_DEBUG, "Waiting for telnet terminal negotiation");
+				while (telnet_remote_option[TELNET_TERM_TYPE] == TELNET_WILL
+				    || telnet_remote_option[TELNET_TERM_TYPE] == 0) {
+					/* Stop the input thread from writing to the telnet_* vars */
+					pthread_mutex_lock(&input_thread_mutex);
+					if (telnet_remote_option[TELNET_NEGOTIATE_WINDOW_SIZE] == TELNET_WILL) {
+						if (telnet_cols >= TERM_COLS_MIN && telnet_cols <= TERM_COLS_MAX)
+							term->cols = telnet_cols;
+						if (telnet_rows >= TERM_ROWS_MIN && telnet_rows <= TERM_ROWS_MAX)
+							term->rows = telnet_rows;
+					}
+					if (telnet_terminal[0]) {
+						pthread_mutex_unlock(&input_thread_mutex);
+						SAFECOPY(terminal, telnet_terminal);
+						break;
+					}
+					pthread_mutex_unlock(&input_thread_mutex);
+					if (++loops >= 30)
+						break;
+					if (telnet_remote_option[TELNET_TERM_TYPE] == 0 && loops >= 10)
+						break;
+					mswait(100);    // Allow some time for Telnet negotiation
+				}
+				lprintf(LOG_DEBUG, "Telnet terminal negotiation wait complete (%u loops)", loops);
+			}
+		}
 		rioctl(IOFI);       /* flush input buffer */
 		safe_snprintf(str, sizeof(str), "%s  %s", VERSION_NOTICE, COPYRIGHT_NOTICE);
+		if (strcmp(terminal, "PETSCII") == 0) {
+			autoterm |= PETSCII;
+			update_terminal(this);
+		}
 		if (autoterm & PETSCII) {
 			SAFECOPY(terminal, "PETSCII");
 			outchar(FF);
-			center(str);
+			term->center(str);
+			term_out("\r\n");
 		} else {    /* ANSI+ terminal detection */
-			putcom( "\r\n"      /* locate cursor at column 1 */
+			/*
+			 * TODO: Once this merges, it would be good to split the "ANSI detection"
+			 *       out from feature detection (rows/cols/UTF8/CTerm/RIP/etc) so the
+			 *       ANSI_Terminal class can be responsible for collecting the
+			 *       details of the ANSI implementation.
+			 */
+			term_out( "\r\n"      /* locate cursor at column 1 */
 			        "\x1b[s"    /* save cursor position (necessary for HyperTerm auto-ANSI) */
 			        "\x1b[0c"   /* Request CTerm version */
 			        "\x1b[255B" /* locate cursor as far down as possible */
@@ -576,9 +618,9 @@ bool sbbs_t::answer()
 			        "\r"        /* Move cursor left (in case previous char printed) */
 			        );
 			i = l = 0;
-			row = 0;
-			lncntr = 0;
-			center(str);
+			term->row = 0;
+			term->lncntr = 0;
+			term->center(str);
 
 			while (i++ < 50 && l < (int)sizeof(str) - 1) {     /* wait up to 5 seconds for response */
 				c = incom(100) & 0x7f;
@@ -634,9 +676,9 @@ bool sbbs_t::answer()
 						if (cursor_pos_report == 1) {
 							/* Sanity check the coordinates in the response: */
 							if (x >= TERM_COLS_MIN && x <= TERM_COLS_MAX)
-								cols = x;
+								term->cols = x;
 							if (y >= TERM_ROWS_MIN && y <= TERM_ROWS_MAX)
-								rows = y;
+								term->rows = y;
 						} else {    // second report
 							if (x < 3) { // ZWNBSP didn't move cursor (more than one column)
 								autoterm |= UTF8;
@@ -645,7 +687,7 @@ bool sbbs_t::answer()
 						}
 					} else if (sscanf(p, "[=67;84;101;114;109;%u;%u", &x, &y) == 2 && *lastchar(p) == 'c') {
 						lprintf(LOG_INFO, "received CTerm version report: %u.%u", x, y);
-						cterm_version = (x * 1000) + y;
+						term->cterm_version = (x * 1000) + y;
 					}
 					p = strtok_r(NULL, "\x1b", &tokenizer);
 				}
@@ -661,7 +703,7 @@ bool sbbs_t::answer()
 				}
 			}
 			if (terminal[0])
-				lprintf(LOG_DEBUG, "auto-detected terminal type: %ux%u %s", cols, rows, terminal);
+				lprintf(LOG_DEBUG, "auto-detected terminal type: %ux%u %s", term->cols, term->rows, terminal);
 			else
 				SAFECOPY(terminal, "DUMB");
 		}
@@ -733,9 +775,9 @@ bool sbbs_t::answer()
 				if (telnet_terminal[0])
 					SAFECOPY(terminal, telnet_terminal);
 				if (telnet_cols >= TERM_COLS_MIN && telnet_cols <= TERM_COLS_MAX)
-					cols = telnet_cols;
+					term->cols = telnet_cols;
 				if (telnet_rows >= TERM_ROWS_MIN && telnet_rows <= TERM_ROWS_MAX)
-					rows = telnet_rows;
+					term->rows = telnet_rows;
 			} else {
 				lprintf(LOG_NOTICE, "no Telnet commands received, reverting to Raw TCP mode");
 				telnet_mode |= TELNET_MODE_OFF;
@@ -746,7 +788,7 @@ bool sbbs_t::answer()
 			}
 			pthread_mutex_unlock(&input_thread_mutex);
 		}
-		lprintf(LOG_INFO, "terminal type: %ux%u %s %s", cols, rows, term_charset(autoterm), terminal);
+		lprintf(LOG_INFO, "terminal type: %ux%u %s %s", term->cols, term->rows, term_charset(autoterm), terminal);
 		SAFECOPY(client_ipaddr, cid);   /* Over-ride IP address with Caller-ID info */
 		SAFECOPY(useron.comp, client_name);
 	}
diff --git a/src/sbbs3/atcodes.cpp b/src/sbbs3/atcodes.cpp
index 3d2f3e08db6da1e3f701bacaf966a19436417eb8..b155a5faef20b2aa0e0a153ca9329d2c74ffcdc8 100644
--- a/src/sbbs3/atcodes.cpp
+++ b/src/sbbs3/atcodes.cpp
@@ -143,7 +143,7 @@ int sbbs_t::show_atcode(const char *instr, JSObject* obj)
 			tp++;
 		}
 		c_unescape_str(tp);
-		add_hotspot(tp, /* hungry: */ true, column, column + strlen(sp) - 1, row);
+		term->add_hotspot(tp, /* hungry: */ true, term->column, term->column + strlen(sp) - 1, term->row);
 		bputs(sp);
 		return len;
 	}
@@ -158,7 +158,7 @@ int sbbs_t::show_atcode(const char *instr, JSObject* obj)
 			tp++;
 		}
 		c_unescape_str(tp);
-		add_hotspot(tp, /* hungry: */ false, column, column + strlen(sp) - 1, row);
+		term->add_hotspot(tp, /* hungry: */ false, term->column, term->column + strlen(sp) - 1, term->row);
 		bputs(sp);
 		return len;
 	}
@@ -194,15 +194,15 @@ int sbbs_t::show_atcode(const char *instr, JSObject* obj)
 		fmt.align = fmt.left;
 
 	if (fmt.truncated && strchr(cp, '\n') == NULL) {
-		if (column + fmt.disp_len > cols - 1) {
-			if (column >= cols - 1)
+		if (term->column + fmt.disp_len > term->cols - 1) {
+			if (term->column >= term->cols - 1)
 				fmt.disp_len = 0;
 			else
-				fmt.disp_len = (cols - 1) - column;
+				fmt.disp_len = (term->cols - 1) - term->column;
 		}
 	}
 	if (pmode & P_UTF8) {
-		if (term_supports(UTF8))
+		if (term->charset() == CHARSET_UTF8)
 			fmt.disp_len += strlen(cp) - utf8_str_total_width(cp, unicode_zerowidth);
 		else
 			fmt.disp_len += strlen(cp) - utf8_str_count_width(cp, /* min: */ 1, /* max: */ 2, unicode_zerowidth);
@@ -344,7 +344,7 @@ const char* sbbs_t::atcode(const char* sp, char* str, size_t maxlen, int* pmode,
 		return nulstr;
 	}
 	if (strcmp(sp, "CLEAR_HOT") == 0) {
-		clear_hotspots();
+		term->clear_hotspots();
 		return nulstr;
 	}
 
@@ -371,18 +371,18 @@ const char* sbbs_t::atcode(const char* sp, char* str, size_t maxlen, int* pmode,
 	if (strncmp(sp, "U+", 2) == 0) { // UNICODE
 		enum unicode_codepoint codepoint = (enum unicode_codepoint)strtoul(sp + 2, &tp, 16);
 		if (tp == NULL || *tp == 0)
-			outchar(codepoint, unicode_to_cp437(codepoint));
+			outcp(codepoint, unicode_to_cp437(codepoint));
 		else if (*tp == ':')
-			outchar(codepoint, tp + 1);
+			outcp(codepoint, tp + 1);
 		else {
 			char fallback = (char)strtoul(tp + 1, NULL, 16);
 			if (*tp == ',')
-				outchar(codepoint, fallback);
+				outcp(codepoint, fallback);
 			else if (*tp == '!') {
 				char ch = unicode_to_cp437(codepoint);
 				if (ch != 0)
 					fallback = ch;
-				outchar(codepoint, fallback);
+				outcp(codepoint, fallback);
 			}
 			else
 				return NULL;  // Invalid @-code
@@ -391,36 +391,36 @@ const char* sbbs_t::atcode(const char* sp, char* str, size_t maxlen, int* pmode,
 	}
 
 	if (strcmp(sp, "CHECKMARK") == 0) {
-		outchar(UNICODE_CHECK_MARK, CP437_CHECK_MARK);
+		outcp(UNICODE_CHECK_MARK, CP437_CHECK_MARK);
 		return nulstr;
 	}
 
 	if (strcmp(sp, "ELLIPSIS") == 0) {
-		outchar(UNICODE_HORIZONTAL_ELLIPSIS, "...");
+		outcp(UNICODE_HORIZONTAL_ELLIPSIS, "...");
 		return nulstr;
 	}
 	if (strcmp(sp, "COPY") == 0) {
-		outchar(UNICODE_COPYRIGHT_SIGN, "(C)");
+		outcp(UNICODE_COPYRIGHT_SIGN, "(C)");
 		return nulstr;
 	}
 	if (strcmp(sp, "SOUNDCOPY") == 0) {
-		outchar(UNICODE_SOUND_RECORDING_COPYRIGHT, "(P)");
+		outcp(UNICODE_SOUND_RECORDING_COPYRIGHT, "(P)");
 		return nulstr;
 	}
 	if (strcmp(sp, "REGISTERED") == 0) {
-		outchar(UNICODE_REGISTERED_SIGN, "(R)");
+		outcp(UNICODE_REGISTERED_SIGN, "(R)");
 		return nulstr;
 	}
 	if (strcmp(sp, "TRADEMARK") == 0) {
-		outchar(UNICODE_TRADE_MARK_SIGN, "(TM)");
+		outcp(UNICODE_TRADE_MARK_SIGN, "(TM)");
 		return nulstr;
 	}
 	if (strcmp(sp, "DEGREE_C") == 0) {
-		outchar(UNICODE_DEGREE_CELSIUS, "\xF8""C");
+		outcp(UNICODE_DEGREE_CELSIUS, "\xF8""C");
 		return nulstr;
 	}
 	if (strcmp(sp, "DEGREE_F") == 0) {
-		outchar(UNICODE_DEGREE_FAHRENHEIT, "\xF8""F");
+		outcp(UNICODE_DEGREE_FAHRENHEIT, "\xF8""F");
 		return nulstr;
 	}
 
@@ -525,7 +525,7 @@ const char* sbbs_t::atcode(const char* sp, char* str, size_t maxlen, int* pmode,
 		return cfg.sys_name;
 
 	if (!strcmp(sp, "BAUD") || !strcmp(sp, "BPS")) {
-		safe_snprintf(str, maxlen, "%u", cur_output_rate ? cur_output_rate : cur_rate);
+		safe_snprintf(str, maxlen, "%u", term->cur_output_rate ? term->cur_output_rate : cur_rate);
 		return str;
 	}
 
@@ -535,18 +535,18 @@ const char* sbbs_t::atcode(const char* sp, char* str, size_t maxlen, int* pmode,
 	}
 
 	if (!strcmp(sp, "COLS")) {
-		safe_snprintf(str, maxlen, "%u", cols);
+		safe_snprintf(str, maxlen, "%u", term->cols);
 		return str;
 	}
 	if (!strcmp(sp, "ROWS")) {
-		safe_snprintf(str, maxlen, "%u", rows);
+		safe_snprintf(str, maxlen, "%u", term->rows);
 		return str;
 	}
 	if (strcmp(sp, "TERM") == 0)
 		return term_type();
 
 	if (strcmp(sp, "CHARSET") == 0)
-		return term_charset();
+		return term->charset_str();
 
 	if (!strcmp(sp, "CONN"))
 		return connection;
@@ -625,7 +625,7 @@ const char* sbbs_t::atcode(const char* sp, char* str, size_t maxlen, int* pmode,
 		return useron.netmail;
 
 	if (strcmp(sp, "TERMTYPE") == 0)
-		return term_type(&useron, term_supports(), str, maxlen);
+		return term_type(&useron, term->flags(), str, maxlen);
 
 	if (strcmp(sp, "TERMROWS") == 0)
 		return term_rows(&useron, str, maxlen);
@@ -655,8 +655,8 @@ const char* sbbs_t::atcode(const char* sp, char* str, size_t maxlen, int* pmode,
 		return (useron.misc & PETSCII) ? text[On] : text[Off];
 
 	if (strcmp(sp, "PETGRFX") == 0) {
-		if (term_supports(PETSCII))
-			outcom(PETSCII_UPPERGRFX);
+		if (term->charset() == CHARSET_PETSCII)
+			term_out(PETSCII_UPPERGRFX);
 		return nulstr;
 	}
 
@@ -956,7 +956,7 @@ const char* sbbs_t::atcode(const char* sp, char* str, size_t maxlen, int* pmode,
 	}
 
 	if (!strcmp(sp, "RESETPAUSE")) {
-		lncntr = 0;
+		term->lncntr = 0;
 		return nulstr;
 	}
 
@@ -972,11 +972,11 @@ const char* sbbs_t::atcode(const char* sp, char* str, size_t maxlen, int* pmode,
 
 	if (strncmp(sp, "FILL:", 5) == 0) {
 		SAFECOPY(tmp, sp + 5);
-		int margin = centered ? column : 1;
+		int margin = centered ? term->column : 1;
 		if (margin < 1)
 			margin = 1;
 		c_unescape_str(tmp);
-		while (*tmp && online && column < cols - margin)
+		while (*tmp && online && term->column < term->cols - margin)
 			bputs(tmp, P_TRUNCATE);
 		return nulstr;
 	}
@@ -985,7 +985,7 @@ const char* sbbs_t::atcode(const char* sp, char* str, size_t maxlen, int* pmode,
 		i = atoi(sp + 4);
 		if (i >= 1)  // Convert to 0-based
 			i--;
-		for (l = i - column; l > 0; l--)
+		for (l = i - term->column; l > 0; l--)
 			outchar(' ');
 		return nulstr;
 	}
@@ -1011,7 +1011,7 @@ const char* sbbs_t::atcode(const char* sp, char* str, size_t maxlen, int* pmode,
 	}
 
 	if (strncmp(sp, "BPS:", 4) == 0) {
-		set_output_rate((enum output_rate)atoi(sp + 4));
+		term->set_output_rate((enum output_rate)atoi(sp + 4));
 		return nulstr;
 	}
 
@@ -1638,52 +1638,52 @@ const char* sbbs_t::atcode(const char* sp, char* str, size_t maxlen, int* pmode,
 		return "\r\n";
 
 	if (!strcmp(sp, "PUSHXY")) {
-		ansi_save();
+		term->save_cursor_pos();
 		return nulstr;
 	}
 
 	if (!strcmp(sp, "POPXY")) {
-		ansi_restore();
+		term->restore_cursor_pos();
 		return nulstr;
 	}
 
 	if (!strcmp(sp, "HOME")) {
-		cursor_home();
+		term->cursor_home();
 		return nulstr;
 	}
 
 	if (!strcmp(sp, "CLRLINE")) {
-		clearline();
+		term->clearline();
 		return nulstr;
 	}
 
 	if (!strcmp(sp, "CLR2EOL") || !strcmp(sp, "CLREOL")) {
-		cleartoeol();
+		term->cleartoeol();
 		return nulstr;
 	}
 
 	if (!strcmp(sp, "CLR2EOS")) {
-		cleartoeos();
+		term->cleartoeos();
 		return nulstr;
 	}
 
 	if (!strncmp(sp, "UP:", 3)) {
-		cursor_up(atoi(sp + 3));
+		term->cursor_up(atoi(sp + 3));
 		return str;
 	}
 
 	if (!strncmp(sp, "DOWN:", 5)) {
-		cursor_down(atoi(sp + 5));
+		term->cursor_down(atoi(sp + 5));
 		return str;
 	}
 
 	if (!strncmp(sp, "LEFT:", 5)) {
-		cursor_left(atoi(sp + 5));
+		term->cursor_left(atoi(sp + 5));
 		return str;
 	}
 
 	if (!strncmp(sp, "RIGHT:", 6)) {
-		cursor_right(atoi(sp + 6));
+		term->cursor_right(atoi(sp + 6));
 		return str;
 	}
 
@@ -1691,7 +1691,7 @@ const char* sbbs_t::atcode(const char* sp, char* str, size_t maxlen, int* pmode,
 		const char* cp = strchr(sp, ',');
 		if (cp != NULL) {
 			cp++;
-			cursor_xy(atoi(sp + 7), atoi(cp));
+			term->gotoxy(atoi(sp + 7), atoi(cp));
 		}
 		return nulstr;
 	}
diff --git a/src/sbbs3/bat_xfer.cpp b/src/sbbs3/bat_xfer.cpp
index f8c9a6b56ee78720f6abae5a204544aaf4886405..23f0b139021e42937f59f60bd6fb75f7d032746c 100644
--- a/src/sbbs3/bat_xfer.cpp
+++ b/src/sbbs3/bat_xfer.cpp
@@ -49,14 +49,14 @@ void sbbs_t::batchmenu()
 	}
 	if (useron.misc & (RIP) && !(useron.misc & EXPERT))
 		menu("batchxfr");
-	lncntr = 0;
+	term->lncntr = 0;
 	while (online && (cfg.upload_dir != INVALID_DIR || batdn_total() || batup_total())) {
 		if (!(useron.misc & (EXPERT | RIP))) {
 			sys_status &= ~SS_ABORT;
-			if (lncntr) {
+			if (term->lncntr) {
 				sync();
 				CRLF;
-				if (lncntr)          /* CRLF or SYNC can cause pause */
+				if (term->lncntr)          /* CRLF or SYNC can cause pause */
 					pause();
 			}
 			menu("batchxfr");
@@ -68,7 +68,7 @@ void sbbs_t::batchmenu()
 		if (ch > ' ')
 			logch(ch, 0);
 		if (ch == quit_key() || ch == '\r') {    /* Quit */
-			lncntr = 0;
+			term->lncntr = 0;
 			break;
 		}
 		switch (ch) {
@@ -368,7 +368,7 @@ bool sbbs_t::start_batch_download()
 		curdirnum = batdn_dir[i];         /* for ARS */
 		unpadfname(batdn_name[i], fname);
 		if (cfg.dir[batdn_dir[i]]->seqdev) {
-			lncntr = 0;
+			term->lncntr = 0;
 			SAFEPRINTF2(path, "%s%s", cfg.temp_dir, fname);
 			if (!fexistcase(path)) {
 				seqwait(cfg.dir[batdn_dir[i]]->seqdev);
@@ -548,7 +548,7 @@ bool sbbs_t::process_batch_upload_queue()
 		const char* filename = filenames[i];
 		int         dir = batch_file_dir(&cfg, ini, filename);
 		curdirnum = dir; /* for ARS */
-		lncntr = 0; /* defeat pause */
+		term->lncntr = 0; /* defeat pause */
 
 		SAFEPRINTF2(src, "%s%s", cfg.temp_dir, filename);
 		SAFEPRINTF2(dest, "%s%s", cfg.dir[dir]->path, filename);
@@ -639,7 +639,7 @@ void sbbs_t::batch_download(int xfrprot)
 
 	for (size_t i = 0; filenames[i] != NULL; ++i) {
 		char* filename = filenames[i];
-		lncntr = 0;                               /* defeat pause */
+		term->lncntr = 0;                               /* defeat pause */
 		if (xfrprot == -1 || checkprotresult(cfg.prot[xfrprot], 0, filename)) {
 			file_t f = {{}};
 			if (!batch_file_load(&cfg, ini, filename, &f)) {
@@ -680,7 +680,7 @@ void sbbs_t::batch_add_list(char *list)
 			if (!fgets(str, sizeof(str) - 1, stream))
 				break;
 			truncnl(str);
-			lncntr = 0;
+			term->lncntr = 0;
 			for (i = j = k = 0; i < usrlibs; i++) {
 				for (j = 0; j < usrdirs[i]; j++, k++) {
 					outchar('.');
diff --git a/src/sbbs3/bulkmail.cpp b/src/sbbs3/bulkmail.cpp
index d5cd78eeb37ac118fab1c852eaa4a70fb7bb39eb..57b705bc6ed3fd96f45d6fefe1304feaa43627e7 100644
--- a/src/sbbs3/bulkmail.cpp
+++ b/src/sbbs3/bulkmail.cpp
@@ -193,7 +193,7 @@ int sbbs_t::bulkmailhdr(smb_t* smb, smbmsg_t* msg, uint usernum)
 	if (j != SMB_SUCCESS)
 		return j;
 
-	lncntr = 0;
+	term->lncntr = 0;
 	bprintf(text[Emailing], user.alias, usernum);
 	SAFEPRINTF2(str, "bulk-mailed %s #%d"
 	            , user.alias, usernum);
diff --git a/src/sbbs3/chat.cpp b/src/sbbs3/chat.cpp
index b9069fbd0dc668ce6041de77abf7538fb1782714..6eb629bf227eac749e9e0f6fb3fd7d4560d5ff6b 100644
--- a/src/sbbs3/chat.cpp
+++ b/src/sbbs3/chat.cpp
@@ -410,7 +410,7 @@ void sbbs_t::multinodechat(int channel)
 						    ? text[AnonUserChatHandle]
 						    : useron.handle
 						         , cfg.node_num, ':', nulstr);
-						snprintf(tmp, sizeof tmp, "%*s", (int)bstrlen(str), nulstr);
+						snprintf(tmp, sizeof tmp, "%*s", (int)term->bstrlen(str), nulstr);
 						SAFECAT(pgraph, tmp);
 					}
 					SAFECAT(pgraph, line);
@@ -528,7 +528,7 @@ void sbbs_t::multinodechat(int channel)
 		if (sys_status & SS_ABORT)
 			break;
 	}
-	lncntr = 0;
+	term->lncntr = 0;
 	if (gurubuf != NULL)
 		free(gurubuf);
 }
@@ -681,17 +681,19 @@ bool sbbs_t::chan_access(int cnum)
 void sbbs_t::privchat(bool forced, int node_num)
 {
 	char str[128], c, *p, localbuf[5][81], remotebuf[5][81]
-	, localline = 0, remoteline = 0, localchar = 0, remotechar = 0
+	, localchar = 0, remotechar = 0
 	, *sep = text[PrivateChatSeparator]
 	, *local_sep = text[SysopChatSeparator]
 	;
+	unsigned localline = 0, remoteline = 0;
 	char   tmp[512];
 	char   outpath[MAX_PATH + 1];
 	char   inpath[MAX_PATH + 1];
 	uchar  ch;
 	int    wr;
-	int    in, out, i, n, echo = 1, x, y, activity, remote_activity;
-	int    local_y = 1, remote_y = 1;
+	int    in, out, i, n, echo = 1, activity, remote_activity;
+	unsigned x, y;
+	unsigned local_y = 1, remote_y = 1;
 	node_t node;
 	time_t last_nodechk = 0;
 
@@ -785,7 +787,7 @@ void sbbs_t::privchat(bool forced, int node_num)
 	}
 
 	if (((sys_status & SS_USERON && useron.chat & CHAT_SPLITP) || !(sys_status & SS_USERON))
-	    && term_supports(ANSI) && rows >= 24 && cols >= 80)
+	    && term->can_move() && term->rows >= 24 && term->cols >= 80)
 		sys_status |= SS_SPLITP;
 	else
 		sys_status &= ~SS_SPLITP;
@@ -865,23 +867,23 @@ void sbbs_t::privchat(bool forced, int node_num)
 	sync();
 
 	if (sys_status & SS_SPLITP) {
-		lncntr = 0;
+		term->lncntr = 0;
 		CLS;
-		ansi_save();
-		ansi_gotoxy(1, 13);
+		term->save_cursor_pos();
+		term->gotoxy(1, 13);
 		remote_y = 1;
 		bprintf(forced ? local_sep : sep
 		        , thisnode.misc & NODE_MSGW ? 'T':' '
 		        , sectostr(timeleft, tmp)
 		        , thisnode.misc & NODE_NMSG ? 'M':' ');
-		ansi_gotoxy(1, 14);
+		term->gotoxy(1, 14);
 		local_y = 14;
 	}
 
 	while (online && (forced || !(sys_status & SS_ABORT))) {
-		lncntr = 0;
+		term->lncntr = 0;
 		if (sys_status & SS_SPLITP)
-			lbuflen = 0;
+			term->lbuflen = 0;
 		action = NODE_PCHT;
 		activity = 0;
 		remote_activity = 0;
@@ -892,7 +894,7 @@ void sbbs_t::privchat(bool forced, int node_num)
 			if (ch == BS || ch == DEL) {
 				if (localchar) {
 					if (echo)
-						backspace();
+						term->backspace();
 					localchar--;
 					localbuf[localline][localchar] = 0;
 				}
@@ -913,25 +915,25 @@ void sbbs_t::privchat(bool forced, int node_num)
 					CLS;
 					attr(cfg.color[clr_chatremote]);
 					remotebuf[remoteline][remotechar] = 0;
-					for (i = 0; i <= remoteline; i++) {
-						bputs(remotebuf[i]);
-						if (i != remoteline)
+					for (unsigned u = 0; u <= remoteline; u++) {
+						bputs(remotebuf[u]);
+						if (u != remoteline)
 							bputs(crlf);
 					}
 					remote_y = 1 + remoteline;
 					bputs("\1i_\1n");  /* Fake cursor */
-					ansi_save();
-					ansi_gotoxy(1, 13);
+					term->save_cursor_pos();
+					term->gotoxy(1, 13);
 					bprintf(forced ? local_sep : sep
 					        , thisnode.misc & NODE_MSGW ? 'T':' '
 					        , sectostr(timeleft, tmp)
 					        , thisnode.misc & NODE_NMSG ? 'M':' ');
-					ansi_gotoxy(1, 14);
+					term->gotoxy(1, 14);
 					attr(cfg.color[clr_chatlocal]);
 					localbuf[localline][localchar] = 0;
-					for (i = 0; i <= localline; i++) {
-						bputs(localbuf[i]);
-						if (i != localline)
+					for (unsigned u = 0; u <= localline; u++) {
+						bputs(localbuf[u]);
+						if (u != localline)
 							bputs(crlf);
 					}
 					local_y = 15 + localline;
@@ -951,19 +953,20 @@ void sbbs_t::privchat(bool forced, int node_num)
 					localbuf[localline][localchar] = 0;
 					localchar = 0;
 
-					if (sys_status & SS_SPLITP && local_y >= rows) {
-						ansi_gotoxy(1, 13);
+					if (sys_status & SS_SPLITP && local_y >= term->rows) {
+						term->gotoxy(1, 13);
 						bprintf(forced ? local_sep : sep
 						        , thisnode.misc & NODE_MSGW ? 'T':' '
 						        , sectostr(timeleft, tmp)
 						        , thisnode.misc & NODE_NMSG ? 'M':' ');
 						attr(cfg.color[clr_chatlocal]);
-						for (x = 13, y = 0; x < rows; x++, y++) {
-							comprintf("\x1b[%d;1H\x1b[K", x + 1);
+						for (x = 13, y = 0; x < term->rows; x++, y++) {
+							term->gotoxy(1, x + 1);
+							term->cleartoeol();
 							if (y <= localline)
 								bprintf("%s\r\n", localbuf[y]);
 						}
-						ansi_gotoxy(1, local_y = (15 + localline));
+						term->gotoxy(1, local_y = (15 + localline));
 						localline = 0;
 					}
 					else {
@@ -976,7 +979,7 @@ void sbbs_t::privchat(bool forced, int node_num)
 							CRLF;
 							local_y++;
 							if (sys_status & SS_SPLITP)
-								cleartoeol();
+								term->cleartoeol();
 						}
 					}
 					// sync();
@@ -1032,16 +1035,16 @@ void sbbs_t::privchat(bool forced, int node_num)
 					lprintf(LOG_DEBUG, "read character '%c' from %s", ch, inpath);
 				activity = 1;
 				if (sys_status & SS_SPLITP && !remote_activity) {
-					ansi_getxy(&x, &y);
-					ansi_restore();
+					term->getxy(&x, &y);
+					term->restore_cursor_pos();
 				}
 				attr(cfg.color[clr_chatremote]);
 				if (sys_status & SS_SPLITP && !remote_activity)
-					backspace();         /* Delete fake cursor */
+					term->backspace();         /* Delete fake cursor */
 				remote_activity = 1;
 				if (ch == BS || ch == DEL) {
 					if (remotechar) {
-						backspace();
+						term->backspace();
 						remotechar--;
 						remotebuf[remoteline][remotechar] = 0;
 					}
@@ -1073,13 +1076,14 @@ void sbbs_t::privchat(bool forced, int node_num)
 							        , sectostr(timeleft, tmp)
 							        , thisnode.misc & NODE_NMSG ? 'M':' ');
 							attr(cfg.color[clr_chatremote]);
-							for (i = 0; i < 12; i++) {
-								bprintf("\x1b[%d;1H\x1b[K", i + 1);
-								if (i <= remoteline)
-									bprintf("%s\r\n", remotebuf[i]);
+							for (unsigned u = 0; u < 12; u++) {
+								term->gotoxy(1, u + 1);
+								term->cleartoeol();
+								if (u <= remoteline)
+									bprintf("%s\r\n", remotebuf[u]);
 							}
 							remoteline = 0;
-							ansi_gotoxy(1, remote_y = 6);
+							term->gotoxy(1, remote_y = 6);
 						}
 						else {
 							if (remoteline >= 4)
@@ -1091,7 +1095,7 @@ void sbbs_t::privchat(bool forced, int node_num)
 								CRLF;
 								remote_y++;
 								if (sys_status & SS_SPLITP)
-									cleartoeol();
+									term->cleartoeol();
 							}
 						}
 					}
@@ -1106,8 +1110,8 @@ void sbbs_t::privchat(bool forced, int node_num)
 
 		if (sys_status & SS_SPLITP && remote_activity) {
 			bputs("\1i_\1n");  /* Fake cursor */
-			ansi_save();
-			ansi_gotoxy(x, y);
+			term->save_cursor_pos();
+			term->gotoxy(x, y);
 		}
 
 		now = time(NULL);
@@ -1314,15 +1318,15 @@ void sbbs_t::nodemsg()
 				break;
 			if (getnodedat(cfg.node_num, &thisnode, false)) {
 				if (thisnode.misc & (NODE_MSGW | NODE_NMSG)) {
-					lncntr = 0;   /* prevent pause prompt */
-					saveline();
+					term->lncntr = 0;   /* prevent pause prompt */
+					term->saveline();
 					CRLF;
 					if (thisnode.misc & NODE_NMSG)
 						getnmsg();
 					if (thisnode.misc & NODE_MSGW)
 						getsmsg(useron.number);
 					CRLF;
-					restoreline();
+					term->restoreline();
 				}
 				else
 					nodesync();
@@ -1726,7 +1730,7 @@ void sbbs_t::guruchat(char* line, char* gurubuf, int gurunum, char* last_answer)
 						if (sbbs_random(100)) {
 							mswait(100 + sbbs_random(300));
 							while (c) {
-								backspace();
+								term->backspace();
 								mswait(50 + sbbs_random(50));
 								c--;
 							}
@@ -1922,7 +1926,6 @@ void sbbs_t::localguru(char *gurubuf, int gurunum)
 		return;
 	sys_status |= SS_GURUCHAT;
 	console &= ~(CON_L_ECHOX | CON_R_ECHOX);    /* turn off X's */
-	console |= (CON_L_ECHO | CON_R_ECHO);                   /* make sure echo is on */
 	if (action == NODE_CHAT) { /* only page if from chat section */
 		bprintf(text[PagingGuru], cfg.guru[gurunum]->name);
 		ch = sbbs_random(25) + 25;
diff --git a/src/sbbs3/chk_ar.cpp b/src/sbbs3/chk_ar.cpp
index c73ee739f66232412fb32d6d60267d93be4fff2b..f850a6f10d1fe81f1e3e8c98ddf5b775bed9a5ec 100644
--- a/src/sbbs3/chk_ar.cpp
+++ b/src/sbbs3/chk_ar.cpp
@@ -109,7 +109,7 @@ bool sbbs_t::ar_exp(const uchar **ptrptr, user_t* user, client_t* client)
 				}
 				break;
 			case AR_ANSI:
-				if (!term_supports(ANSI))
+				if (!term->supports(ANSI))
 					result = _not;
 				else
 					result = !_not;
@@ -119,37 +119,37 @@ bool sbbs_t::ar_exp(const uchar **ptrptr, user_t* user, client_t* client)
 				}
 				break;
 			case AR_PETSCII:
-				if ((term_supports() & CHARSET_FLAGS) != CHARSET_PETSCII)
+				if (term->charset() != CHARSET_PETSCII)
 					result = _not;
 				else
 					result = !_not;
 				if (!result) {
 					noaccess_str = text[NoAccessTerminal];
-					noaccess_val = PETSCII;
+					noaccess_val = CHARSET_PETSCII;
 				}
 				break;
 			case AR_ASCII:
-				if ((term_supports() & CHARSET_FLAGS) != CHARSET_ASCII)
+				if (term->charset() != CHARSET_ASCII)
 					result = _not;
 				else
 					result = !_not;
 				if (!result) {
 					noaccess_str = text[NoAccessTerminal];
-					noaccess_val = NO_EXASCII;
+					noaccess_val = CHARSET_ASCII;
 				}
 				break;
 			case AR_UTF8:
-				if ((term_supports() & CHARSET_FLAGS) != CHARSET_UTF8)
+				if (term->charset() != CHARSET_UTF8)
 					result = _not;
 				else
 					result = !_not;
 				if (!result) {
 					noaccess_str = text[NoAccessTerminal];
-					noaccess_val = UTF8;
+					noaccess_val = CHARSET_UTF8;
 				}
 				break;
 			case AR_CP437:
-				if ((term_supports() & CHARSET_FLAGS) != CHARSET_CP437)
+				if (term->charset() != CHARSET_CP437)
 					result = _not;
 				else
 					result = !_not;
@@ -159,7 +159,7 @@ bool sbbs_t::ar_exp(const uchar **ptrptr, user_t* user, client_t* client)
 				}
 				break;
 			case AR_RIP:
-				if (!term_supports(RIP))
+				if (!term->supports(RIP))
 					result = _not;
 				else
 					result = !_not;
@@ -707,7 +707,7 @@ bool sbbs_t::ar_exp(const uchar **ptrptr, user_t* user, client_t* client)
 				}
 				break;
 			case AR_COLS:
-				if ((equal && cols != (long)n) || (!equal && cols < (long)n))
+				if ((equal && term->cols != n) || (!equal && term->cols < n))
 					result = _not;
 				else
 					result = !_not;
@@ -717,7 +717,7 @@ bool sbbs_t::ar_exp(const uchar **ptrptr, user_t* user, client_t* client)
 				}
 				break;
 			case AR_ROWS:
-				if ((equal && rows != (long)n) || (!equal && rows < (long)n))
+				if ((equal && term->rows != n) || (!equal && term->rows < n))
 					result = _not;
 				else
 					result = !_not;
diff --git a/src/sbbs3/con_hi.cpp b/src/sbbs3/con_hi.cpp
index 06efbdaf156865b80e80c825b67e44cf921a4a33..d314d16beda9ec991f621a2320007e9db9ebe544 100644
--- a/src/sbbs3/con_hi.cpp
+++ b/src/sbbs3/con_hi.cpp
@@ -31,19 +31,19 @@ void sbbs_t::redrwstr(char *strin, int i, int l, int mode)
 	if (i <= 0)
 		i = 0;
 	else
-		cursor_left(i);
+		term->cursor_left(i);
 	if (l < 0)
 		l = 0;
 	if (mode)
 		bprintf(mode, "%-*.*s", l, l, strin);
 	else
-		column += rprintf("%-*.*s", l, l, strin);
-	cleartoeol();
+		rprintf("%-*.*s", l, l, strin);
+	term->cleartoeol();
 	if (i < l) {
 		auto_utf8(strin, mode);
 		if (mode & P_UTF8)
 			l = utf8_str_total_width(strin, unicode_zerowidth);
-		cursor_left(l - i);
+		term->cursor_left(l - i);
 	}
 }
 
@@ -62,7 +62,7 @@ int sbbs_t::uselect(bool add, uint n, const char *title, const char *item, const
 		if (!uselect_total)
 			bprintf(text[SelectItemHdr], title);
 		uselect_num[uselect_total++] = n;
-		add_hotspot(uselect_total);
+		term->add_hotspot(uselect_total);
 		bprintf(text[SelectItemFmt], uselect_total, item);
 		return 0;
 	}
@@ -80,7 +80,7 @@ int sbbs_t::uselect(bool add, uint n, const char *title, const char *item, const
 	i = getnum(uselect_total);
 	t = uselect_total;
 	uselect_total = 0;
-	clear_hotspots();
+	term->clear_hotspots();
 	if (i < 0)
 		return -1;
 	if (!i) {                    /* User hit ENTER, use default */
@@ -159,7 +159,7 @@ bool sbbs_t::chksyspass(const char* sys_pw)
 		bputs(text[SystemPassword]);
 		getstr(str, sizeof(cfg.sys_pass) - 1, K_UPPER | K_NOECHO);
 		CRLF;
-		lncntr = 0;
+		term->lncntr = 0;
 	}
 	if (stricmp(cfg.sys_pass, str)) {
 		if (cfg.sys_misc & SM_ECHO_PW)
diff --git a/src/sbbs3/con_out.cpp b/src/sbbs3/con_out.cpp
index a219d293a3c353299bb4bfd4c35b5fd9291fff32..b1e416363d6f22797d90c1d4155fe113c69ee98b 100644
--- a/src/sbbs3/con_out.cpp
+++ b/src/sbbs3/con_out.cpp
@@ -53,12 +53,11 @@ int sbbs_t::bputs(const char *str, int mode)
 {
 	int    i;
 	size_t l = 0;
-	int    term = term_supports();
 
 	if ((mode & P_REMOTE) && online != ON_REMOTE)
 		return 0;
 
-	if (online == ON_LOCAL && console & CON_L_ECHO)  /* script running as event */
+	if (online == ON_LOCAL)  /* script running as event */
 		return lputs(LOG_INFO, str);
 
 	str = auto_utf8(str, mode);
@@ -72,7 +71,7 @@ int sbbs_t::bputs(const char *str, int mode)
 			case CTRL_A:
 				break;
 			default: // printing char
-				if ((mode & P_TRUNCATE) && column >= (cols - 1)) {
+				if ((mode & P_TRUNCATE) && term->column >= (term->cols - 1)) {
 					l++;
 					continue;
 				}
@@ -84,14 +83,14 @@ int sbbs_t::bputs(const char *str, int mode)
 			if (str[l] == '~') { // Mouse hot-spot (hungry)
 				l++;
 				if (str[l] >= ' ')
-					add_hotspot(str[l], /* hungry */ true);
+					term->add_hotspot(str[l], /* hungry */ true);
 				else
-					add_hotspot('\r', /* hungry */ true);
+					term->add_hotspot('\r', /* hungry */ true);
 				continue;
 			}
 			if (str[l] == '`' && str[l + 1] >= ' ') { // Mouse hot-spot (strict)
 				l++;
-				add_hotspot(str[l], /* hungry */ false);
+				term->add_hotspot(str[l], /* hungry */ false);
 				continue;
 			}
 			ctrl_a(str[l++]);
@@ -117,13 +116,16 @@ int sbbs_t::bputs(const char *str, int mode)
 			}
 		}
 		if (mode & P_PETSCII) {
-			if (term & PETSCII)
-				outcom(str[l++]);
+			if (term->charset() == CHARSET_PETSCII)
+				term_out(str[l++]);
 			else
 				petscii_to_ansibbs(str[l++]);
+		} else if (str[l] == '\r' && str[l + 1] == '\n') {
+			term->newline();
+			l += 2;
 		} else if ((str[l] & 0x80) && (mode & P_UTF8)) {
-			if (term & UTF8)
-				outcom(str[l++]);
+			if (term->charset() == CHARSET_UTF8)
+				term_out(str[l++]);
 			else
 				l += print_utf8_as_cp437(str + l, len - l);
 		} else
@@ -132,38 +134,6 @@ int sbbs_t::bputs(const char *str, int mode)
 	return l;
 }
 
-/****************************************************************************/
-/* Returns the printed columns from 'str' accounting for Ctrl-A codes		*/
-/****************************************************************************/
-size_t sbbs_t::bstrlen(const char *str, int mode)
-{
-	str = auto_utf8(str, mode);
-	size_t      count = 0;
-	const char* end = str + strlen(str);
-	while (str < end) {
-		int len = 1;
-		if (*str == CTRL_A) {
-			str++;
-			if (*str == 0 || *str == 'Z')    // EOF
-				break;
-			if (*str == '[') // CR
-				count = 0;
-			else if (*str == '<' && count) // ND-Backspace
-				count--;
-		} else if (((*str) & 0x80) && (mode & P_UTF8)) {
-			enum unicode_codepoint codepoint = UNICODE_UNDEFINED;
-			len = utf8_getc(str, end - str, &codepoint);
-			if (len < 1)
-				break;
-			count += unicode_width(codepoint, unicode_zerowidth);
-		} else
-			count++;
-		str += len;
-	}
-	return count;
-}
-
-
 /* Perform PETSCII terminal output translation (from ASCII/CP437) */
 unsigned char cp437_to_petscii(unsigned char ch)
 {
@@ -252,14 +222,14 @@ int sbbs_t::petscii_to_ansibbs(unsigned char ch)
 	if (IS_ALPHA(ch))
 		return outchar(ch ^ 0x20);  /* swap upper/lower case */
 	switch (ch) {
-		case '\r':                  newline();      break;
-		case PETSCII_HOME:          cursor_home();  break;
+		case '\r':                  term->newline();      break;
+		case PETSCII_HOME:          term->cursor_home();  break;
 		case PETSCII_CLEAR:         return CLS;
-		case PETSCII_DELETE:        backspace();    break;
-		case PETSCII_LEFT:          cursor_left();  break;
-		case PETSCII_RIGHT:         cursor_right(); break;
-		case PETSCII_UP:            cursor_up();    break;
-		case PETSCII_DOWN:          cursor_down();  break;
+		case PETSCII_DELETE:        term->backspace();    break;
+		case PETSCII_LEFT:          term->cursor_left();  break;
+		case PETSCII_RIGHT:         term->cursor_right(); break;
+		case PETSCII_UP:            term->cursor_up();    break;
+		case PETSCII_DOWN:          term->cursor_down();  break;
 
 		case PETSCII_BRITPOUND:     return outchar((char)156);
 		case PETSCII_CHECKMARK:     return outchar((char)251);
@@ -365,50 +335,164 @@ size_t sbbs_t::print_utf8_as_cp437(const char* str, size_t len)
 /* Performs saveline buffering (for restoreline)							*/
 /* DOES NOT expand ctrl-A codes, track columns, lines, auto-pause, etc.     */
 /****************************************************************************/
-int sbbs_t::rputs(const char *str, size_t len)
+size_t sbbs_t::rputs(const char *str, size_t len)
 {
-	size_t l;
+	unsigned oldlc = term->lncntr;
+	size_t ret = cp437_out(str, len ? len : SIZE_MAX);
+	term->lncntr = oldlc;
+	return ret;
+}
 
+/*
+ * Translates from CP437 to the current encoding and passes the result
+ * to term_out()
+ * Returns the number of bytes successfully consumed
+ */
+size_t sbbs_t::cp437_out(const char *str, size_t len)
+{
+	if (len == SIZE_MAX)
+		len = strlen(str);
+	for (size_t l = 0; l < len && online; l++) {
+		if (cp437_out(str[l]) == 0)
+			return l;
+	}
+	return len;
+}
+
+size_t sbbs_t::cp437_out(int ich)
+{
+	unsigned char ch {static_cast<unsigned char>(ich)};
 	if (console & CON_ECHO_OFF)
 		return 0;
-	if (len == 0)
-		len = strlen(str);
-	int  term = term_supports();
-	char utf8[UTF8_MAX_LEN + 1] = "";
-	for (l = 0; l < len && online; l++) {
-		uchar ch = str[l];
-		utf8[0] = 0;
-		if (term & PETSCII) {
-			ch = cp437_to_petscii(ch);
-			if (ch == PETSCII_SOLID)
-				outcom(PETSCII_REVERSE_ON);
-		}
-		else if ((term & NO_EXASCII) && (ch & 0x80))
-			ch = exascii_to_ascii_char(ch);  /* seven bit table */
-		else if (term & UTF8) {
-			enum unicode_codepoint codepoint = cp437_unicode_tbl[(uchar)ch];
-			if (codepoint != 0)
-				utf8_putc(utf8, sizeof(utf8) - 1, codepoint);
+	if (!online)
+		return 0;
+
+	// Many of the low CP437 characters will print on many terminals,
+	// so we may want to translate some of them.
+	// Synchronet specifically has definitions for the following:
+	//
+	// BEL (0x07) Make a beeping noise
+	// BS  (0x08) Move left one character, but do not wrap to
+	//            previous line
+	// LF  (0x0A) Move down one line, scrolling if on bottom row
+	// CR  (0x0D) Move to the first position in the current line
+	//
+	// Further, the following are expected to be translated
+	// earlier and should only arrive here via rputs()
+	// TAB (0x09) Expanded to spaces to the next tabstop (defined by
+	//            sbbs::tabstop)
+	// FF  (0x0C) Clear the screen
+	//
+	// It is unclear what "should" happen when these arrive here,
+	// but it should be consistent, likely they should be expanded.
+	switch (ch) {
+		case '\b':	// BS
+			term->cursor_left();
+			return 1;
+		case '\t':	// TAB
+			if (term->column < (term->cols - 1)) {
+                                outchar(' ');
+                                while ((term->column < (term->cols - 1)) && (term->column % term->tabstop))
+                                        outchar(' ');
+                        }
+                        return 1;
+
+		case '\n':	// LF
+			term->line_feed();
+			return 1;
+		case '\r':	// CR
+			term->carriage_return();
+			return 1;
+		case FF:	// FF
+			term->clearscreen();
+			return 1;
+	}
+
+	// UTF-8 Note that CP437 0x7C maps to U+00A6 so we can't just do
+	//       everything below 0x80 this way.
+	if (term->charset() == CHARSET_UTF8) {
+		if (ch != '\a') {
+			char utf8[UTF8_MAX_LEN + 1];
+			enum unicode_codepoint codepoint = cp437_unicode_tbl[ch];
+			if (codepoint == 0)
+				codepoint = static_cast<enum unicode_codepoint>(ch);
+			int len = utf8_putc(utf8, sizeof(utf8), codepoint);
+
+			if (len < 1)
+				return 0;
+			if (term_out(utf8, len) != (size_t)len)
+				return 0;
+			return 1;
 		}
-		if (utf8[0])
-			putcom(utf8);
-		else {
-			if (outcom(ch) != 0)
-				break;
-			if ((char)ch == (char)TELNET_IAC && !(telnet_mode & TELNET_MODE_OFF))
-				outcom(TELNET_IAC); /* Must escape Telnet IAC char (255) */
-			if ((term & PETSCII) && ch == PETSCII_SOLID)
-				outcom(PETSCII_REVERSE_OFF);
+	}
+	// PETSCII
+	else if (term->charset() == CHARSET_PETSCII) {
+		ch = cp437_to_petscii(ch);
+		// TODO: This needs to be aware of the current state of reverse...
+		//       It could cast sbbs->term to PETSCII_Terminal (ugh)
+		if (ch == PETSCII_SOLID) {
+			if (term_out(PETSCII_REVERSE_ON) != 1)
+				return 0;
 		}
-		if (ch == '\n')
-			lbuflen = 0;
-		else if (lbuflen < LINE_BUFSIZE) {
-			if (lbuflen == 0)
-				latr = curatr;
-			lbuf[lbuflen++] = str[l]; // save non-translated char to line buffer
+		if (term_out(ch) != 1)
+			return 0;
+		if (ch == PETSCII_SOLID) {
+			if (term_out(PETSCII_REVERSE_OFF) != 1)
+				return 0;
 		}
+		return 1;
 	}
-	return l;
+	// CP437 or US-ASCII
+	if ((term->charset() == CHARSET_ASCII) && (ch & 0x80))
+		ch = exascii_to_ascii_char(ch);  /* seven bit table */
+	if (term_out(ch) != 1)
+		return 0;
+	return 1;
+}
+
+/*
+ * Update column, row, line buffer, and line counter
+ * Returns the number of bytes consumed
+ */
+size_t sbbs_t::term_out(const char *str, size_t len)
+{
+	if (len == SIZE_MAX)
+		len = strlen(str);
+	for (size_t l = 0; l < len && online; l++) {
+		uchar ch = str[l];
+		if (!term_out(ch))
+			return l;
+	}
+	return len;
+}
+
+size_t sbbs_t::term_out(int ich)
+{
+	unsigned char ch{static_cast<unsigned char>(ich)};
+	if (console & CON_ECHO_OFF)
+		return 0;
+	if (!online)
+		return 0;
+	// We do this before parse_output() so parse_output() can
+	// prevent \n from ending up at the start of the line buffer.
+	if (term->lbuflen < LINE_BUFSIZE && !term->suspend_lbuf) {
+		if (term->lbuflen == 0)
+			term->latr = curatr;
+		// Historically, beeps don't go into lbuf
+		// hopefully nobody notices and cares, because this would mean
+		// that BEL *must* be part of any charset we support... and it's
+		// not part of C64 PETSCII.
+		term->lbuf[term->lbuflen++] = ch;
+	}
+	if (!term->parse_output(ch))
+		return 1;
+	if (ch == TELNET_IAC && !(telnet_mode & TELNET_MODE_OFF)) {
+		if (outcom(TELNET_IAC))
+			return 0;
+	}
+	if (outcom(ch))
+		return 0;
+	return 1;
 }
 
 /****************************************************************************/
@@ -461,9 +545,9 @@ int sbbs_t::rprintf(const char *fmt, ...)
 }
 
 /****************************************************************************/
-/* Performs printf() using bbs putcom/outcom functions						*/
+/* Performs printf() using bbs term_out functions						*/
 /****************************************************************************/
-int sbbs_t::comprintf(const char *fmt, ...)
+int sbbs_t::term_printf(const char *fmt, ...)
 {
 	va_list argptr;
 	char    sbuf[4096];
@@ -472,61 +556,22 @@ int sbbs_t::comprintf(const char *fmt, ...)
 	vsnprintf(sbuf, sizeof(sbuf), fmt, argptr);
 	sbuf[sizeof(sbuf) - 1] = 0; /* force termination */
 	va_end(argptr);
-	return putcom(sbuf);
-}
-
-/****************************************************************************/
-/* Outputs destructive backspace 											*/
-/****************************************************************************/
-void sbbs_t::backspace(int count)
-{
-	if (count < 1)
-		return;
-	if (!(console & CON_ECHO_OFF)) {
-		for (int i = 0; i < count; i++) {
-			if (term_supports(PETSCII))
-				outcom(PETSCII_DELETE);
-			else {
-				outcom('\b');
-				outcom(' ');
-				outcom('\b');
-			}
-			if (column > 0)
-				column--;
-			if (lbuflen > 0)
-				lbuflen--;
-		}
-	}
-}
-
-/****************************************************************************/
-/* Returns true if the user (or the yet-to-be-logged-in client) supports	*/
-/* all of the specified terminal 'cmp_flags' (e.g. ANSI, COLOR, RIP).		*/
-/* If no flags specified, returns all terminal flag bits supported			*/
-/****************************************************************************/
-int sbbs_t::term_supports(int cmp_flags)
-{
-	int flags = ((sys_status & (SS_USERON | SS_NEWUSER)) && !(useron.misc & AUTOTERM)) ? useron.misc : autoterm;
-
-	if ((sys_status & (SS_USERON | SS_NEWUSER)) && (useron.misc & AUTOTERM))
-		flags |= useron.misc & (NO_EXASCII | SWAP_DELETE | COLOR | ICE_COLOR | MOUSE);
-
-	return cmp_flags ? ((flags & cmp_flags) == cmp_flags) : (flags & TERM_FLAGS);
+	return term_out(sbuf);
 }
 
 char* sbbs_t::term_rows(user_t* user, char* str, size_t size)
 {
 	if (user->rows >= TERM_ROWS_MIN && user->rows <= TERM_ROWS_MAX)
-		rows = user->rows;
-	safe_snprintf(str, size, "%s%d %s", user->rows ? nulstr:text[TerminalAutoDetect], rows, text[TerminalRows]);
+		term->rows = user->rows;
+	safe_snprintf(str, size, "%s%d %s", user->rows ? nulstr:text[TerminalAutoDetect], term->rows, text[TerminalRows]);
 	return str;
 }
 
 char* sbbs_t::term_cols(user_t* user, char* str, size_t size)
 {
 	if (user->cols >= TERM_COLS_MIN && user->cols <= TERM_COLS_MAX)
-		cols = user->cols;
-	safe_snprintf(str, size, "%s%d %s", user->cols ? nulstr:text[TerminalAutoDetect], cols, text[TerminalColumns]);
+		term->cols = user->cols;
+	safe_snprintf(str, size, "%s%d %s", user->cols ? nulstr:text[TerminalAutoDetect], term->cols, text[TerminalColumns]);
 	return str;
 }
 
@@ -556,7 +601,7 @@ char* sbbs_t::term_type(user_t* user, int term, char* str, size_t size)
 const char* sbbs_t::term_type(int term)
 {
 	if (term == -1)
-		term = term_supports();
+		term = this->term->flags();
 	if (term & PETSCII)
 		return "PETSCII";
 	if (term & RIP)
@@ -572,7 +617,7 @@ const char* sbbs_t::term_type(int term)
 const char* sbbs_t::term_charset(int term)
 {
 	if (term == -1)
-		term = term_supports();
+		return this->term->charset_str();
 	if (term & PETSCII)
 		return "CBM-ASCII";
 	if (term & UTF8)
@@ -587,13 +632,14 @@ const char* sbbs_t::term_charset(int term)
 /****************************************************************************/
 bool sbbs_t::update_nodeterm(void)
 {
+	update_terminal(this);
 	str_list_t ini = strListInit();
-	iniSetInteger(&ini, ROOT_SECTION, "cols", cols, NULL);
-	iniSetInteger(&ini, ROOT_SECTION, "rows", rows, NULL);
+	iniSetInteger(&ini, ROOT_SECTION, "cols", term->cols, NULL);
+	iniSetInteger(&ini, ROOT_SECTION, "rows", term->rows, NULL);
 	iniSetString(&ini, ROOT_SECTION, "desc", terminal, NULL);
 	iniSetString(&ini, ROOT_SECTION, "type", term_type(), NULL);
-	iniSetString(&ini, ROOT_SECTION, "chars", term_charset(), NULL);
-	iniSetHexInt(&ini, ROOT_SECTION, "flags", term_supports(), NULL);
+	iniSetString(&ini, ROOT_SECTION, "chars", term->charset_str(), NULL);
+	iniSetHexInt(&ini, ROOT_SECTION, "flags", term->flags(), NULL);
 	iniSetHexInt(&ini, ROOT_SECTION, "mouse", mouse_mode, NULL);
 	iniSetHexInt(&ini, ROOT_SECTION, "console", console, NULL);
 
@@ -612,12 +658,12 @@ bool sbbs_t::update_nodeterm(void)
 		char topic[128];
 		SAFEPRINTF(topic, "node/%u/terminal", cfg.node_num);
 		snprintf(str, sizeof(str), "%u\t%u\t%s\t%s\t%s\t%x\t%x\t%x"
-		         , cols
-		         , rows
+		         , term->cols
+		         , term->rows
 		         , terminal
 		         , term_type()
-		         , term_charset()
-		         , term_supports()
+		         , term->charset_str()
+		         , term->flags()
 		         , mouse_mode
 		         , console
 		         );
@@ -626,9 +672,48 @@ bool sbbs_t::update_nodeterm(void)
 	return result;
 }
 
+/*
+ * bputs  putmsg
+ *   \      /
+ *   outchar    rputs
+ *      +---------|
+ *            cp437_out     outcp
+ *                +-----------+
+ * putcom      term_out
+ *    +-----------+
+ *              outcom
+ *                |
+ *           RingBufWrite
+ * 
+ * In this model:
+ * bputs() and putmsg() call term_out to bypass charset translations
+ * (ie: PETSCII and UTF-8 output)
+ * 
+ * outchar() does tab, FF, rainbow, and auto-pause
+ * 
+ * rputs() effectively just calls cp437_out() and keeps the line counter
+ *         unchanged.  It's used by console.print(), so keeping the
+ *         behaviour unchanged may be important.
+ * 
+ * cp437_out() translates from cp437 to the terminal charset this
+ *             specifically includes the control characters
+ *             BEL, BS, TAB, LF, FF, CR, and DEL
+ * 
+ * outcp() calls term_out() if UTF-8 is supported, or bputs() if not
+ * 
+ * putcom() is used not only for text, but also to send telnet commands.
+ *          The use for text could be replaced, but the use for IAC
+ *          needs to be unchanged.
+ * 
+ * term_out() updates column and row, and maintains the line buffer
+ * 
+ * outcom() and RingBufWrite() are post-IAC expansion
+ */
+
 /****************************************************************************/
 /* Outputs character														*/
-/* Performs terminal translations (e.g. EXASCII-to-ASCII, FF->ESC[2J)		*/
+/* Performs charset translations (e.g. EXASCII-to-ASCII, CP437-to-PETSCII)	*/
+/* Performs terminal expansions and state parsing (e.g. FF to ESC[2JESC[H)	*/
 /* Performs Telnet IAC escaping												*/
 /* Performs tab expansion													*/
 /* Performs column counting, line counting, and auto-pausing				*/
@@ -638,46 +723,8 @@ int sbbs_t::outchar(char ch)
 {
 	if (console & CON_ECHO_OFF)
 		return 0;
-	if (ch == ESC && outchar_esc < ansiState_string)
-		outchar_esc = ansiState_esc;
-	else if (outchar_esc == ansiState_esc) {
-		if (ch == '[')
-			outchar_esc = ansiState_csi;
-		else if (ch == '_' || ch == 'P' || ch == '^' || ch == ']')
-			outchar_esc = ansiState_string;
-		else if (ch == 'X')
-			outchar_esc = ansiState_sos;
-		else if (ch >= '@' && ch <= '_')
-			outchar_esc = ansiState_final;
-		else
-			outchar_esc = ansiState_none;
-	}
-	else if (outchar_esc == ansiState_csi) {
-		if (ch >= '@' && ch <= '~')
-			outchar_esc = ansiState_final;
-	}
-	else if (outchar_esc == ansiState_string) {  // APS, DCS, PM, or OSC
-		if (ch == ESC)
-			outchar_esc = ansiState_esc;
-		if (!((ch >= '\b' && ch <= '\r') || (ch >= ' ' && ch <= '~')))
-			outchar_esc = ansiState_none;
-	}
-	else if (outchar_esc == ansiState_sos) { // SOS
-		if (ch == ESC)
-			outchar_esc = ansiState_sos_esc;
-	}
-	else if (outchar_esc == ansiState_sos_esc) { // ESC inside SOS
-		if (ch == '\\')
-			outchar_esc = ansiState_esc;
-		else if (ch == 'X')
-			outchar_esc = ansiState_none;
-		else
-			outchar_esc = ansiState_sos;
-	}
-	else
-		outchar_esc = ansiState_none;
 
-	if (outchar_esc == ansiState_none && rainbow_index >= 0) {
+	if (rainbow_index >= 0) {
 		attr(rainbow[rainbow_index]);
 		if (rainbow[rainbow_index + 1] == 0) {
 			if (rainbow_repeat)
@@ -685,127 +732,60 @@ int sbbs_t::outchar(char ch)
 		} else
 			++rainbow_index;
 	}
-	int  term = term_supports();
-	char utf8[UTF8_MAX_LEN + 1] = "";
-	if (!(term & PETSCII)) {
-		if ((term & NO_EXASCII) && (ch & 0x80))
-			ch = exascii_to_ascii_char(ch);  /* seven bit table */
-		else if (term & UTF8) {
-			enum unicode_codepoint codepoint = cp437_unicode_tbl[(uchar)ch];
-			if (codepoint != 0)
-				utf8_putc(utf8, sizeof(utf8) - 1, codepoint);
+	if ((console & CON_R_ECHOX) && (uchar)ch >= ' ')
+		ch = *text[PasswordChar];
+
+	if (ch == '\n' && line_delay)
+		SLEEP(line_delay);
+
+	/*
+	 * When line counter overflows, pause on the next pause-eligable line
+	 * and log a debug message
+	 */
+	if (term->lncntr >= term->rows - 1 && term->column == 0) {
+		unsigned lost = term->lncntr - (term->rows - 1);
+		term->lncntr = term->rows -1;
+		if (check_pause()) {
+			if (lost)
+				lprintf(LOG_DEBUG, "line counter overflowed, %u lines scrolled", lost);
 		}
 	}
 
-	if (ch == FF && lncntr > 0 && row > 0) {
-		lncntr = 0;
-		newline();
+	if (ch == FF && term->lncntr > 0 && term->row > 0) {
+		term->lncntr = 0;
+		term->newline();
 		if (!(sys_status & SS_PAUSEOFF)) {
 			pause();
-			while (lncntr && online && !(sys_status & SS_ABORT))
+			while (term->lncntr && online && !(sys_status & SS_ABORT))
 				pause();
 		}
 	}
 
-	if (!(console & CON_R_ECHO))
-		return 0;
+	cp437_out(ch);
 
-	if ((console & CON_R_ECHOX) && (uchar)ch >= ' ' && outchar_esc == ansiState_none) {
-		ch = *text[PasswordChar];
-	}
-	if (ch == FF)
-		clearscreen(term);
-	else if (ch == '\t') {
-		outcom(' ');
-		column++;
-		while (column % tabstop) {
-			outcom(' ');
-			column++;
-		}
-	}
-	else {
-		if (ch == (char)TELNET_IAC && !(telnet_mode & TELNET_MODE_OFF))
-			outcom(TELNET_IAC); /* Must escape Telnet IAC char (255) */
-		if (ch == '\r' && (console & CON_CR_CLREOL))
-			cleartoeol();
-		if (ch == '\n' && line_delay)
-			SLEEP(line_delay);
-		if (term & PETSCII) {
-			uchar pet = cp437_to_petscii(ch);
-			if (pet == PETSCII_SOLID)
-				outcom(PETSCII_REVERSE_ON);
-			outcom(pet);
-			if (pet == PETSCII_SOLID)
-				outcom(PETSCII_REVERSE_OFF);
-			if (ch == '\r' && (curatr & 0xf0) != 0) // reverse video is disabled upon CR
-				curatr >>= 4;
-		} else {
-			if (utf8[0] != 0)
-				putcom(utf8);
-			else
-				outcom(ch);
-		}
-	}
-	if (outchar_esc == ansiState_none) {
-		/* Track cursor position locally */
-		switch (ch) {
-			case '\a':  // 7
-			case '\t':  // 9
-				/* Non-printing or handled elsewhere */
-				break;
-			case '\b':  // 8
-				if (column > 0)
-					column--;
-				if (lbuflen < LINE_BUFSIZE) {
-					if (lbuflen == 0)
-						latr = curatr;
-					lbuf[lbuflen++] = ch;
-				}
-				break;
-			case '\n':  // 10
-				inc_row(1);
-				if (lncntr || lastlinelen)
-					lncntr++;
-				lbuflen = 0;
-				break;
-			case FF:    // 12
-				lncntr = 0;
-				lbuflen = 0;
-				row = 0;
-				column = 0;
-			case '\r':  // 13
-				lastlinelen = column;
-				column = 0;
-				break;
-			default:
-				inc_column(1);
-				if (!lbuflen)
-					latr = curatr;
-				if (lbuflen < LINE_BUFSIZE)
-					lbuf[lbuflen++] = ch;
-				break;
-		}
-	}
-	if (outchar_esc == ansiState_final)
-		outchar_esc = ansiState_none;
+	check_pause();
+	return 0;
+}
 
-	if (lncntr == rows - 1 && ((useron.misc & (UPAUSE ^ (console & CON_PAUSEOFF))) || sys_status & SS_PAUSEON)
+bool sbbs_t::check_pause() {
+	if (term->lncntr == term->rows - 1 && ((useron.misc & (UPAUSE ^ (console & CON_PAUSEOFF))) || sys_status & SS_PAUSEON)
 	    && !(sys_status & (SS_PAUSEOFF | SS_ABORT))) {
-		lncntr = 0;
+		term->lncntr = 0;
 		pause();
+		return true;
 	}
-	return 0;
+	return false;
 }
 
-int sbbs_t::outchar(enum unicode_codepoint codepoint, const char* cp437_fallback)
+int sbbs_t::outcp(enum unicode_codepoint codepoint, const char* cp437_fallback)
 {
-	if (term_supports(UTF8)) {
+	if (term->charset() == CHARSET_UTF8) {
 		char str[UTF8_MAX_LEN];
 		int  len = utf8_putc(str, sizeof(str), codepoint);
 		if (len < 1)
 			return len;
-		putcom(str, len);
-		inc_column(unicode_width(codepoint, unicode_zerowidth));
+		term_out(str, len);
+		term->inc_column(unicode_width(codepoint, unicode_zerowidth));
 		return 0;
 	}
 	if (cp437_fallback == NULL)
@@ -813,60 +793,17 @@ int sbbs_t::outchar(enum unicode_codepoint codepoint, const char* cp437_fallback
 	return bputs(cp437_fallback);
 }
 
-int sbbs_t::outchar(enum unicode_codepoint codepoint, char cp437_fallback)
+int sbbs_t::outcp(enum unicode_codepoint codepoint, char cp437_fallback)
 {
 	char str[2] = { cp437_fallback, '\0' };
-	return outchar(codepoint, str);
-}
-
-void sbbs_t::inc_column(int count)
-{
-	column += count;
-	if (column >= cols) {    // assume terminal has/will auto-line-wrap
-		lncntr++;
-		lbuflen = 0;
-		lastlinelen = column;
-		column = 0;
-		inc_row(1);
-	}
-}
-
-void sbbs_t::inc_row(int count)
-{
-	row += count;
-	if (row >= rows) {
-		scroll_hotspots((row - rows) + 1);
-		row = rows - 1;
-	}
-}
-
-void sbbs_t::center(const char *instr, bool msg, unsigned int columns)
-{
-	char   str[256];
-	size_t len;
-
-	if (columns < 1)
-		columns = cols;
-
-	SAFECOPY(str, instr);
-	truncsp(str);
-	len = bstrlen(str);
-	carriage_return();
-	if (len < columns)
-		cursor_right((columns - len) / 2);
-	if (msg)
-		putmsg(str, P_NONE);
-	else
-		bputs(str);
-	newline();
+	return outcp(codepoint, str);
 }
 
 void sbbs_t::wide(const char* str)
 {
-	int term = term_supports();
 	while (*str != '\0') {
-		if ((term & UTF8) && *str >= '!' && *str <= '~')
-			outchar((enum unicode_codepoint)(UNICODE_FULLWIDTH_EXCLAMATION_MARK + (*str - '!')));
+		if ((term->charset() == CHARSET_UTF8) && *str >= '!' && *str <= '~')
+			outcp((enum unicode_codepoint)(UNICODE_FULLWIDTH_EXCLAMATION_MARK + (*str - '!')));
 		else {
 			outchar(*str);
 			outchar(' ');
@@ -875,244 +812,17 @@ void sbbs_t::wide(const char* str)
 	}
 }
 
-
-// Send a bare carriage return, hopefully moving the cursor to the far left, current row
-void sbbs_t::carriage_return(int count)
-{
-	if (count < 1)
-		return;
-	for (int i = 0; i < count; i++) {
-		if (term_supports(PETSCII))
-			cursor_left(column);
-		else
-			outcom('\r');
-		column = 0;
-	}
-}
-
-// Send a bare line_feed, hopefully moving the cursor down one row, current column
-void sbbs_t::line_feed(int count)
-{
-	if (count < 1)
-		return;
-	for (int i = 0; i < count; i++) {
-		if (term_supports(PETSCII))
-			outcom(PETSCII_DOWN);
-		else
-			outcom('\n');
-	}
-	inc_row(count);
-}
-
-void sbbs_t::newline(int count)
-{
-	if (count < 1)
-		return;
-	for (int i = 0; i < count; i++) {
-		outchar('\r');
-		outchar('\n');
-	}
-}
-
-void sbbs_t::clearscreen(int term)
-{
-	clear_hotspots();
-	if (term & ANSI)
-		putcom("\x1b[2J\x1b[H");    /* clear screen, home cursor */
-	else if (term & PETSCII)
-		outcom(PETSCII_CLEAR);
-	else
-		outcom(FF);
-	row = 0;
-	column = 0;
-	lncntr = 0;
-}
-
-void sbbs_t::clearline(void)
-{
-	carriage_return();
-	cleartoeol();
-}
-
-void sbbs_t::cursor_home(void)
-{
-	int term = term_supports();
-	if (term & ANSI)
-		putcom("\x1b[H");
-	else if (term & PETSCII)
-		outcom(PETSCII_HOME);
-	else
-		outchar(FF);    /* this will clear some terminals, do nothing with others */
-	row = 0;
-	column = 0;
-}
-
-void sbbs_t::cursor_up(int count)
-{
-	if (count < 1)
-		return;
-	int term = term_supports();
-	if (term & ANSI) {
-		if (count > 1)
-			comprintf("\x1b[%dA", count);
-		else
-			putcom("\x1b[A");
-	} else {
-		if (term & PETSCII) {
-			for (int i = 0; i < count; i++)
-				outcom(PETSCII_UP);
-		}
-	}
-}
-
-void sbbs_t::cursor_down(int count)
-{
-	if (count < 1)
-		return;
-	if (term_supports(ANSI)) {
-		if (count > 1)
-			comprintf("\x1b[%dB", count);
-		else
-			putcom("\x1b[B");
-		inc_row(count);
-	} else {
-		for (int i = 0; i < count; i++)
-			line_feed();
-	}
-}
-
-void sbbs_t::cursor_right(int count)
-{
-	if (count < 1)
-		return;
-	int term = term_supports();
-	if (term & ANSI) {
-		if (count > 1)
-			comprintf("\x1b[%dC", count);
-		else
-			putcom("\x1b[C");
-	} else {
-		for (int i = 0; i < count; i++) {
-			if (term & PETSCII)
-				outcom(PETSCII_RIGHT);
-			else
-				outcom(' ');
-		}
-	}
-	column += count;
-}
-
-void sbbs_t::cursor_left(int count)
-{
-	if (count < 1)
-		return;
-	int term = term_supports();
-	if (term & ANSI) {
-		if (count > 1)
-			comprintf("\x1b[%dD", count);
-		else
-			putcom("\x1b[D");
-	} else {
-		for (int i = 0; i < count; i++) {
-			if (term & PETSCII)
-				outcom(PETSCII_LEFT);
-			else
-				outcom('\b');
-		}
-	}
-	if (column > count)
-		column -= count;
-	else
-		column = 0;
-}
-
-bool sbbs_t::cursor_xy(int x, int y)
-{
-	int term = term_supports();
-	if (term & ANSI)
-		return ansi_gotoxy(x, y);
-	if (term & PETSCII) {
-		outcom(PETSCII_HOME);
-		cursor_down(y - 1);
-		cursor_right(x - 1);
-		return true;
-	}
-	return false;
-}
-
-bool sbbs_t::cursor_getxy(int* x, int* y)
-{
-	if (term_supports(ANSI))
-		return ansi_getxy(x, y);
-	*x = column + 1;
-	*y = row + 1;
-	return true;
-}
-
-void sbbs_t::cleartoeol(void)
-{
-	int i, j;
-
-	int term = term_supports();
-	if (term & ANSI)
-		putcom("\x1b[K");
-	else {
-		i = j = column;
-		while (++i <= cols)
-			outcom(' ');
-		while (++j <= cols) {
-			if (term & PETSCII)
-				outcom(PETSCII_LEFT);
-			else
-				outcom('\b');
-		}
-	}
-}
-
-void sbbs_t::cleartoeos(void)
-{
-	if (term_supports(ANSI))
-		putcom("\x1b[J");
-}
-
-void sbbs_t::set_output_rate(enum output_rate speed)
-{
-	if (term_supports(ANSI)) {
-		unsigned int val = speed;
-		switch (val) {
-			case 0:     val = 0; break;
-			case 600:   val = 2; break;
-			case 1200:  val = 3; break;
-			case 2400:  val = 4; break;
-			case 4800:  val = 5; break;
-			case 9600:  val = 6; break;
-			case 19200: val = 7; break;
-			case 38400: val = 8; break;
-			case 57600: val = 9; break;
-			case 76800: val = 10; break;
-			default:
-				if (val <= 300)
-					val = 1;
-				else if (val > 76800)
-					val = 11;
-				break;
-		}
-		comprintf("\x1b[;%u*r", val);
-		cur_output_rate = speed;
-	}
-}
-
 /****************************************************************************/
 /* Get the dimensions of the current user console, place into row and cols	*/
 /****************************************************************************/
 void sbbs_t::getdimensions()
 {
 	if (sys_status & SS_USERON) {
-		ansi_getdims();
+		term->getdims();
 		if (useron.rows >= TERM_ROWS_MIN && useron.rows <= TERM_ROWS_MAX)
-			rows = useron.rows;
+			term->rows = useron.rows;
 		if (useron.cols >= TERM_COLS_MIN && useron.cols <= TERM_COLS_MAX)
-			cols = useron.cols;
+			term->cols = useron.cols;
 	}
 }
 
@@ -1174,7 +884,7 @@ void sbbs_t::ctrl_a(char x)
 		return;
 
 	if ((uchar)x > 0x7f) {
-		cursor_right((uchar)x - 0x7f);
+		term->cursor_right((uchar)x - 0x7f);
 		return;
 	}
 	if (valid_ctrl_a_attr(x))
@@ -1202,7 +912,7 @@ void sbbs_t::ctrl_a(char x)
 			pause();
 			break;
 		case 'Q':   /* Pause reset */
-			lncntr = 0;
+			term->lncntr = 0;
 			break;
 		case 'T':   /* Time */
 			now = time(NULL);
@@ -1233,35 +943,35 @@ void sbbs_t::ctrl_a(char x)
 			sync();
 			break;
 		case 'J':   /* clear to end-of-screen */
-			cleartoeos();
+			term->cleartoeos();
 			break;
 		case 'L':   /* CLS (form feed) */
 			CLS;
 			break;
 		case '\'':  /* Home cursor */
 		case '`':   // usurped by strict hot-spot
-			cursor_home();
+			term->cursor_home();
 			break;
 		case '>':   /* CLREOL */
-			cleartoeol();
+			term->cleartoeol();
 			break;
 		case '<':   /* Non-destructive backspace */
-			cursor_left();
+			term->cursor_left();
 			break;
 		case '/':   /* Conditional new-line */
-			cond_newline();
+			term->cond_newline();
 			break;
 		case '\\':  /* Conditional New-line / Continuation prefix (if cols < 80) */
-			cond_contline();
+			term->cond_contline();
 			break;
 		case '?':   /* Conditional blank-line */
-			cond_blankline();
+			term->cond_blankline();
 			break;
 		case '[':   /* Carriage return */
-			carriage_return();
+			term->carriage_return();
 			break;
 		case ']':   /* Line feed */
-			line_feed();
+			term->line_feed();
 			break;
 		case 'A':   /* Ctrl-A */
 			outchar(CTRL_A);
@@ -1274,12 +984,10 @@ void sbbs_t::ctrl_a(char x)
 			attr(atr);
 			break;
 		case 'I':
-			if ((term_supports() & (ICE_COLOR | PETSCII)) != ICE_COLOR)
-				attr(atr | BLINK);
+			attr(atr | BLINK);
 			break;
 		case 'E': /* Bright Background */
-			if (term_supports() & (ICE_COLOR | PETSCII))
-				attr(atr | BG_BRIGHT);
+			attr(atr | BG_BRIGHT);
 			break;
 		case 'F':   /* Blink, only if alt Blink Font is loaded */
 			if (((atr & HIGH) && (console & CON_HBLINK_FONT)) || (!(atr & HIGH) && (console & CON_BLINK_FONT)))
@@ -1359,78 +1067,11 @@ void sbbs_t::ctrl_a(char x)
 /****************************************************************************/
 int sbbs_t::attr(int atr)
 {
-	char str[16];
-	int  newatr = atr;
+	char str[128];
 
-	int  term = term_supports();
-	if (term & PETSCII) {
-		if (atr & (0x70 | BG_BRIGHT)) {  // background color (reverse video for PETSCII)
-			if (atr & BG_BRIGHT)
-				atr |= HIGH;
-			else
-				atr &= ~HIGH;
-			atr = (atr & (BLINK | HIGH)) | ((atr & 0x70) >> 4);
-			outcom(PETSCII_REVERSE_ON);
-		} else
-			outcom(PETSCII_REVERSE_OFF);
-		if (atr & BLINK)
-			outcom(PETSCII_FLASH_ON);
-		else
-			outcom(PETSCII_FLASH_OFF);
-		switch (atr & 0x0f) {
-			case BLACK:
-				outcom(PETSCII_BLACK);
-				break;
-			case WHITE:
-				outcom(PETSCII_WHITE);
-				break;
-			case DARKGRAY:
-				outcom(PETSCII_DARKGRAY);
-				break;
-			case LIGHTGRAY:
-				outcom(PETSCII_LIGHTGRAY);
-				break;
-			case BLUE:
-				outcom(PETSCII_BLUE);
-				break;
-			case LIGHTBLUE:
-				outcom(PETSCII_LIGHTBLUE);
-				break;
-			case CYAN:
-				outcom(PETSCII_MEDIUMGRAY);
-				break;
-			case LIGHTCYAN:
-				outcom(PETSCII_CYAN);
-				break;
-			case YELLOW:
-				outcom(PETSCII_YELLOW);
-				break;
-			case BROWN:
-				outcom(PETSCII_BROWN);
-				break;
-			case RED:
-				outcom(PETSCII_RED);
-				break;
-			case LIGHTRED:
-				outcom(PETSCII_LIGHTRED);
-				break;
-			case GREEN:
-				outcom(PETSCII_GREEN);
-				break;
-			case LIGHTGREEN:
-				outcom(PETSCII_LIGHTGREEN);
-				break;
-			case MAGENTA:
-				outcom(PETSCII_ORANGE);
-				break;
-			case LIGHTMAGENTA:
-				outcom(PETSCII_PURPLE);
-				break;
-		}
-	}
-	else if (term & ANSI)
-		rputs(ansi(newatr, curatr, str));
-	curatr = newatr;
+	term->attrstr(atr, str, sizeof(str));
+	term_out(str);
+	curatr = atr;
 	return 0;
 }
 
@@ -1463,7 +1104,7 @@ int sbbs_t::backfill(const char* instr, float pct, int full_attr, int empty_attr
 	char* str = strip_ctrl(instr, NULL);
 
 	len = strlen(str);
-	if (!(term_supports() & (ANSI | PETSCII)))
+	if (!(term->can_highlight()))
 		bputs(str, P_REMOTE);
 	else {
 		for (int i = 0; i < len; i++) {
@@ -1493,51 +1134,8 @@ void sbbs_t::progress(const char* text, int count, int total, int interval)
 	if (text == NULL)
 		text = "";
 	float pct = total ? ((float)count / total) * 100.0F : 100.0F;
-	SAFEPRINTF2(str, "[ %-8s  %4.1f%% ]", text, pct);
-	cond_newline();
-	cursor_left(backfill(str, pct, cfg.color[clr_progress_full], cfg.color[clr_progress_empty]));
+	SAFEPRINTF2(str, "[ %-8s %5.1f%% ]", text, pct);
+	term->cond_newline();
+	term->cursor_left(backfill(str, pct, cfg.color[clr_progress_full], cfg.color[clr_progress_empty]));
 	last_progress = now;
 }
-
-struct savedline {
-	char buf[LINE_BUFSIZE + 1];     /* Line buffer (i.e. ANSI-encoded) */
-	uint beg_attr;                  /* Starting attribute of each line */
-	uint end_attr;                  /* Ending attribute of each line */
-	int column;                     /* Current column number */
-};
-
-bool sbbs_t::saveline(void)
-{
-	struct savedline line;
-#ifdef _DEBUG
-	lprintf(LOG_DEBUG, "Saving %d chars, cursor at col %d: '%.*s'", lbuflen, column, lbuflen, lbuf);
-#endif
-	line.beg_attr = latr;
-	line.end_attr = curatr;
-	line.column = column;
-	snprintf(line.buf, sizeof(line.buf), "%.*s", lbuflen, lbuf);
-	TERMINATE(line.buf);
-	lbuflen = 0;
-	return listPushNodeData(&savedlines, &line, sizeof(line)) != NULL;
-}
-
-bool sbbs_t::restoreline(void)
-{
-	struct savedline* line = (struct savedline*)listPopNode(&savedlines);
-	if (line == NULL)
-		return false;
-#ifdef _DEBUG
-	lprintf(LOG_DEBUG, "Restoring %d chars, cursor at col %d: '%s'", (int)strlen(line->buf), line->column, line->buf);
-#endif
-	lbuflen = 0;
-	attr(line->beg_attr);
-	rputs(line->buf);
-	if (term_supports(PETSCII))
-		column = strlen(line->buf);
-	curatr = line->end_attr;
-	carriage_return();
-	cursor_right(line->column);
-	free(line);
-	insert_indicator();
-	return true;
-}
diff --git a/src/sbbs3/data.cpp b/src/sbbs3/data.cpp
index 4fe3115bd0c5ff8f64c46e007b8ce4abf7bf6a50..f2c70bd427ad63be59094560d51ca3116e6dd580 100644
--- a/src/sbbs3/data.cpp
+++ b/src/sbbs3/data.cpp
@@ -222,7 +222,7 @@ uint sbbs_t::gettimeleft(bool handle_out_of_time)
 
 		if (!timeleft && !SYSOP && !(sys_status & SS_LCHAT)) {
 			logline(LOG_NOTICE, nulstr, "Ran out of time");
-			saveline();
+			term->saveline();
 			if (sys_status & SS_EVENT)
 				bprintf(text[ReducedTime], timestr(event_time));
 			bputs(text[TimesUp]);
@@ -237,7 +237,7 @@ uint sbbs_t::gettimeleft(bool handle_out_of_time)
 					logline("$-", str);
 					SAFEPRINTF(str, "Minute Adjustment: %u", cfg.cdt_min_value);
 					logline("*+", str);
-					restoreline();
+					term->restoreline();
 					gettimeleft();
 					gettimeleft_inside = 0;
 					return timeleft;
@@ -287,7 +287,7 @@ uint sbbs_t::gettimeleft(bool handle_out_of_time)
 				putuserflags(useron.number, USER_REST, useron.rest);
 				if (cfg.expire_mod[0])
 					exec_bin(cfg.expire_mod, &main_csi);
-				restoreline();
+				term->restoreline();
 				gettimeleft();
 				gettimeleft_inside = 0;
 				return timeleft;
diff --git a/src/sbbs3/download.cpp b/src/sbbs3/download.cpp
index d8a437d31a8bf2dbb1a61b426b55503874af0aa9..39addebc002ea9877304b595e1a62cbd0908d485 100644
--- a/src/sbbs3/download.cpp
+++ b/src/sbbs3/download.cpp
@@ -211,7 +211,7 @@ void sbbs_t::autohangup()
 	rioctl(IOFI);
 	if (!autohang)
 		return;
-	lncntr = 0;
+	term->lncntr = 0;
 	bputs(text[Disconnecting]);
 	attr(GREEN);
 	outchar('[');
diff --git a/src/sbbs3/exec.cpp b/src/sbbs3/exec.cpp
index 2a13fe8e2259f6315660a71e4ef990981f9ccaca..ba555119b3647d50c7f8b5772c5dec5157420330 100644
--- a/src/sbbs3/exec.cpp
+++ b/src/sbbs3/exec.cpp
@@ -229,11 +229,11 @@ int32_t * sbbs_t::getintvar(csi_t *bin, uint32_t name)
 		case 0x1c4455ee:
 			return (int32_t *)&dte_rate;
 		case 0x7fbf958e:
-			return (int32_t *)&lncntr;
+			return (int32_t *)&term->lncntr;
 //		case 0x5c1c1500:
 //			return((int32_t *)&tos);
 		case 0x613b690e:
-			return (int32_t *)&rows;
+			return (int32_t *)&term->rows;
 		case 0x205ace36:
 			return (int32_t *)&autoterm;
 		case 0x7d0ed0d1:
@@ -1328,24 +1328,18 @@ int sbbs_t::exec(csi_t *csi)
 				lputs(LOG_INFO, cmdstr((char*)csi->ip, path, csi->str, (char*)buf));
 				break;
 			case CS_PRINT_REMOTE:
-				putcom(cmdstr((char*)csi->ip, path, csi->str, (char*)buf));
+				term_out(cmdstr((char*)csi->ip, path, csi->str, (char*)buf));
 				break;
 			case CS_PRINTFILE:
 				printfile(cmdstr((char*)csi->ip, path, csi->str, (char*)buf), P_SAVEATR);
 				break;
 			case CS_PRINTFILE_REMOTE:
-				if (online != ON_REMOTE || !(console & CON_R_ECHO))
+				if (online != ON_REMOTE)
 					break;
-				console &= ~CON_L_ECHO;
 				printfile(cmdstr((char*)csi->ip, path, csi->str, (char*)buf), P_SAVEATR);
-				console |= CON_L_ECHO;
 				break;
 			case CS_PRINTFILE_LOCAL:
-				if (!(console & CON_L_ECHO))
-					break;
-				console &= ~CON_R_ECHO;
-				printfile(cmdstr((char*)csi->ip, path, csi->str, (char*)buf), P_SAVEATR);
-				console |= CON_R_ECHO;
+				lprintf(LOG_WARNING, "PRINTFILE_LOCAL is no longer functional");
 				break;
 			case CS_CHKFILE:
 				csi->logic = !fexistcase(cmdstr((char*)csi->ip, path, csi->str, (char*)buf));
@@ -1829,7 +1823,7 @@ int sbbs_t::exec(csi_t *csi)
 			pause();
 			return 0;
 		case CS_PAUSE_RESET:
-			lncntr = 0;
+			term->lncntr = 0;
 			return 0;
 		case CS_GETLINES:
 			getdimensions();
@@ -1902,10 +1896,10 @@ int sbbs_t::exec(csi_t *csi)
 				csi->logic = LOGIC_FALSE;
 			return 0;
 		case CS_SAVELINE:
-			saveline();
+			term->saveline();
 			return 0;
 		case CS_RESTORELINE:
-			restoreline();
+			term->restoreline();
 			return 0;
 		case CS_SELECT_SHELL:
 			csi->logic = select_shell() ? LOGIC_TRUE:LOGIC_FALSE;
diff --git a/src/sbbs3/execfile.cpp b/src/sbbs3/execfile.cpp
index 0eaa0012b1f3a74188042b2076e650952a6414ef..fb3bc26eaac10b019b3b65de05f892fb45e76d0e 100644
--- a/src/sbbs3/execfile.cpp
+++ b/src/sbbs3/execfile.cpp
@@ -50,7 +50,7 @@ int sbbs_t::exec_file(csi_t *csi)
 								outchar(' ');
 							if (i < 99)
 								outchar(' ');
-							add_hotspot(i + 1);
+							term->add_hotspot(i + 1);
 							bprintf(text[CfgLibLstFmt]
 							        , i + 1, cfg.lib[usrlib[i]]->lname);
 						}
@@ -58,7 +58,7 @@ int sbbs_t::exec_file(csi_t *csi)
 					snprintf(str, sizeof str, text[JoinWhichLib], curlib + 1);
 					mnemonics(str);
 					j = getnum(usrlibs);
-					clear_hotspots();
+					term->clear_hotspots();
 					if ((int)j == -1)
 						return 0;
 					if (!j)
@@ -82,14 +82,14 @@ int sbbs_t::exec_file(csi_t *csi)
 							outchar(' ');
 						if (i < 99)
 							outchar(' ');
-						add_hotspot(i + 1);
+						term->add_hotspot(i + 1);
 						bputs(str);
 					}
 				}
 				snprintf(str, sizeof str, text[JoinWhichDir], curdir[j] + 1);
 				mnemonics(str);
 				i = getnum(usrdirs[j]);
-				clear_hotspots();
+				term->clear_hotspots();
 				if ((int)i == -1) {
 					if (usrlibs == 1)
 						return 0;
@@ -210,7 +210,7 @@ int sbbs_t::exec_file(csi_t *csi)
 					outchar(' ');
 				if (i < 9)
 					outchar(' ');
-				add_hotspot(i + 1);
+				term->add_hotspot(i + 1);
 				bprintf(text[LibLstFmt], i + 1
 				        , cfg.lib[usrlib[i]]->lname, nulstr, usrdirs[i]);
 			}
@@ -237,7 +237,7 @@ int sbbs_t::exec_file(csi_t *csi)
 					outchar(' ');
 				if (i < 99)
 					outchar(' ');
-				add_hotspot(i + 1);
+				term->add_hotspot(i + 1);
 				bputs(str);
 			}
 			return 0;
diff --git a/src/sbbs3/execmsg.cpp b/src/sbbs3/execmsg.cpp
index a0b836c578945c1a3f54fd1711cb767236b4639f..94db3e742b05f52ffffdc6e757386fb8a33883fd 100644
--- a/src/sbbs3/execmsg.cpp
+++ b/src/sbbs3/execmsg.cpp
@@ -47,7 +47,7 @@ int sbbs_t::exec_msg(csi_t *csi)
 								outchar(' ');
 							if (i < 99)
 								outchar(' ');
-							add_hotspot(i + 1);
+							term->add_hotspot(i + 1);
 							bprintf(text[CfgGrpLstFmt]
 							        , i + 1, cfg.grp[usrgrp[i]]->lname);
 						}
@@ -55,7 +55,7 @@ int sbbs_t::exec_msg(csi_t *csi)
 					snprintf(str, sizeof str, text[JoinWhichGrp], curgrp + 1);
 					mnemonics(str);
 					j = getnum(usrgrps);
-					clear_hotspots();
+					term->clear_hotspots();
 					if ((int)j == -1)
 						return 0;
 					if (!j)
@@ -79,14 +79,14 @@ int sbbs_t::exec_msg(csi_t *csi)
 							outchar(' ');
 						if (i < 99)
 							outchar(' ');
-						add_hotspot(i + 1);
+						term->add_hotspot(i + 1);
 						bputs(str);
 					}
 				}
 				snprintf(str, sizeof str, text[JoinWhichSub], cursub[j] + 1);
 				mnemonics(str);
 				i = getnum(usrsubs[j]);
-				clear_hotspots();
+				term->clear_hotspots();
 				if ((int)i == -1) {
 					if (usrgrps == 1)
 						return 0;
@@ -218,7 +218,7 @@ int sbbs_t::exec_msg(csi_t *csi)
 					outchar(' ');
 				if (i < 9)
 					outchar(' ');
-				add_hotspot(i + 1);
+				term->add_hotspot(i + 1);
 				bprintf(text[GrpLstFmt], i + 1
 				        , cfg.grp[usrgrp[i]]->lname, nulstr, usrsubs[i]);
 			}
@@ -245,7 +245,7 @@ int sbbs_t::exec_msg(csi_t *csi)
 					outchar(' ');
 				if (i < 99)
 					outchar(' ');
-				add_hotspot(i + 1);
+				term->add_hotspot(i + 1);
 				bputs(str);
 			}
 			return 0;
diff --git a/src/sbbs3/extdeps.mk b/src/sbbs3/extdeps.mk
index 8d16bc80934cd7508947a968a73f7c12081e4996..ea4abb7fb18c279fc7443d0f4ec66cba551fe9eb 100644
--- a/src/sbbs3/extdeps.mk
+++ b/src/sbbs3/extdeps.mk
@@ -38,7 +38,6 @@ $(MTOBJODIR)/ssl$(OFILE): $(JS_LIB) $(CRYPT_LIB)
 $(MTOBJODIR)/websrvr$(OFILE): $(JS_LIB) $(CRYPT_LIB)
 
 # C++
-$(MTOBJODIR)/ansiterm$(OFILE): $(JS_LIB) $(CRYPT_LIB)
 $(MTOBJODIR)/answer$(OFILE): $(JS_LIB) $(CRYPT_LIB)
 $(MTOBJODIR)/atcodes$(OFILE): $(JS_LIB) $(CRYPT_LIB)
 $(MTOBJODIR)/bat_xfer$(OFILE): $(JS_LIB) $(CRYPT_LIB)
diff --git a/src/sbbs3/file.cpp b/src/sbbs3/file.cpp
index 19bc9555f4c18fa6307226b7436a16b481749133..d871ccee7ca4f3135693e771d1a4e0ad933c270a 100644
--- a/src/sbbs3/file.cpp
+++ b/src/sbbs3/file.cpp
@@ -103,7 +103,7 @@ void sbbs_t::showfileinfo(file_t* f, bool show_extdesc)
 		SKIP_CRLF(p);
 		truncsp(p);
 		putmsg(p, P_NOATCODES | P_CPM_EOF | P_AUTO_UTF8);
-		newline();
+		term->newline();
 	}
 	if (f->size == -1) {
 		bprintf(text[FileIsNotOnline], f->name);
diff --git a/src/sbbs3/getkey.cpp b/src/sbbs3/getkey.cpp
index ce7c855394c70a60f8d00a43775d284c19d67df8..e07d2931e8bfd659cc05cc5cad4f48b1cb3022c1 100644
--- a/src/sbbs3/getkey.cpp
+++ b/src/sbbs3/getkey.cpp
@@ -45,7 +45,7 @@ char sbbs_t::getkey(int mode)
 	sys_status &= ~SS_ABORT;
 	if ((sys_status & SS_USERON || action == NODE_DFLT) && !(mode & (K_GETSTR | K_NOSPIN)))
 		mode |= (useron.misc & SPIN);
-	lncntr = 0;
+	term->lncntr = 0;
 	getkey_last_activity = time(NULL);
 #if !defined SPINNING_CURSOR_OVER_HARDWARE_CURSOR
 	if (mode & K_SPIN)
@@ -57,7 +57,7 @@ char sbbs_t::getkey(int mode)
 #if defined SPINNING_CURSOR_OVER_HARDWARE_CURSOR
 				bputs(" \b");
 #else
-				backspace();
+				term->backspace();
 #endif
 			}
 			return 0;
@@ -87,7 +87,7 @@ char sbbs_t::getkey(int mode)
 #if defined SPINNING_CURSOR_OVER_HARDWARE_CURSOR
 				bputs(" \b");
 #else
-				backspace();
+				term->backspace();
 #endif
 			}
 			if (mode & K_COLD && ch > ' ' && useron.misc & COLDKEYS) {
@@ -97,7 +97,7 @@ char sbbs_t::getkey(int mode)
 					outchar(ch);
 				while ((coldkey = inkey(mode, 1000)) == 0 && online && !(sys_status & SS_ABORT))
 					;
-				backspace();
+				term->backspace();
 				if (coldkey == BS || coldkey == DEL)
 					continue;
 				if (coldkey > ' ')
@@ -111,18 +111,17 @@ char sbbs_t::getkey(int mode)
 			gettimeleft();
 		else if (online && now - answertime > SEC_LOGON && !(sys_status & SS_LCHAT)) {
 			console &= ~(CON_R_ECHOX | CON_L_ECHOX);
-			console |= (CON_R_ECHO | CON_L_ECHO);
 			bputs(text[TakenTooLongToLogon]);
 			hangup();
 		}
 		if (sys_status & SS_USERON && online && (timeleft / 60) < (5 - timeleft_warn)
 		    && !SYSOP && !(sys_status & SS_LCHAT)) {
 			timeleft_warn = 5 - (timeleft / 60);
-			saveline();
+			term->saveline();
 			attr(LIGHTGRAY);
 			bprintf(text[OnlyXminutesLeft]
 			        , ((ushort)timeleft / 60) + 1, (timeleft / 60) ? "s" : nulstr);
-			restoreline();
+			term->restoreline();
 		}
 
 		if (!(startup->options & BBS_OPT_NO_TELNET_GA)
@@ -138,7 +137,7 @@ char sbbs_t::getkey(int mode)
 		    && ((cfg.inactivity_warn && inactive >= cfg.max_getkey_inactivity * (cfg.inactivity_warn / 100.0))
 		        || inactive >= cfg.max_getkey_inactivity)) {
 			if ((sys_status & SS_USERON) && inactive < cfg.max_getkey_inactivity && *text[AreYouThere] != '\0') {
-				saveline();
+				term->saveline();
 				bputs(text[AreYouThere]);
 			}
 			else
@@ -148,7 +147,6 @@ char sbbs_t::getkey(int mode)
 			}
 			if (now - getkey_last_activity >= cfg.max_getkey_inactivity) {
 				if (online == ON_REMOTE) {
-					console |= CON_R_ECHO;
 					console &= ~CON_R_ECHOX;
 				}
 				bputs(text[CallBackWhenYoureThere]);
@@ -158,9 +156,9 @@ char sbbs_t::getkey(int mode)
 			}
 			if ((sys_status & SS_USERON) && *text[AreYouThere] != '\0') {
 				attr(LIGHTGRAY);
-				carriage_return();
-				cleartoeol();
-				restoreline();
+				term->carriage_return();
+				term->cleartoeol();
+				term->restoreline();
 			}
 			getkey_last_activity = now;
 		}
@@ -200,38 +198,37 @@ void sbbs_t::mnemonics(const char *instr)
 	char str[256];
 	expand_atcodes(instr, str, sizeof str);
 	l = 0L;
-	int  term = term_supports();
 	attr(mneattr_low);
 
 	while (str[l]) {
 		if (str[l] == '~' && str[l + 1] < ' ') {
-			add_hotspot('\r', /* hungry: */ true);
+			term->add_hotspot('\r', /* hungry: */ true);
 			l += 2;
 		}
 		else if (str[l] == '~') {
-			if (!(term & (ANSI | PETSCII)))
+			if (!(term->can_highlight()))
 				outchar('(');
 			l++;
 			if (!ctrl_a_codes)
 				attr(mneattr_high);
-			add_hotspot(str[l], /* hungry: */ true);
+			term->add_hotspot(str[l], /* hungry: */ true);
 			outchar(str[l]);
 			l++;
-			if (!(term & (ANSI | PETSCII)))
+			if (!(term->can_highlight()))
 				outchar(')');
 			if (!ctrl_a_codes)
 				attr(mneattr_low);
 		}
 		else if (str[l] == '`' && str[l + 1] != 0) {
-			if (!(term & (ANSI | PETSCII)))
+			if (!(term->can_highlight()))
 				outchar('[');
 			l++;
 			if (!ctrl_a_codes)
 				attr(mneattr_high);
-			add_hotspot(str[l], /* hungry: */ false);
+			term->add_hotspot(str[l], /* hungry: */ false);
 			outchar(str[l]);
 			l++;
-			if (!(term & (ANSI | PETSCII)))
+			if (!(term->can_highlight()))
 				outchar(']');
 			if (!ctrl_a_codes)
 				attr(mneattr_low);
@@ -275,7 +272,7 @@ bool sbbs_t::yesno(const char *str, int mode)
 				CRLF;
 			if (!(mode & P_SAVEATR))
 				attr(LIGHTGRAY);
-			lncntr = 0;
+			term->lncntr = 0;
 			return true;
 		}
 		if (ch == no_key()) {
@@ -283,7 +280,7 @@ bool sbbs_t::yesno(const char *str, int mode)
 				CRLF;
 			if (!(mode & P_SAVEATR))
 				attr(LIGHTGRAY);
-			lncntr = 0;
+			term->lncntr = 0;
 			return false;
 		}
 	}
@@ -313,7 +310,7 @@ bool sbbs_t::noyes(const char *str, int mode)
 				CRLF;
 			if (!(mode & P_SAVEATR))
 				attr(LIGHTGRAY);
-			lncntr = 0;
+			term->lncntr = 0;
 			return true;
 		}
 		if (ch == yes_key()) {
@@ -321,7 +318,7 @@ bool sbbs_t::noyes(const char *str, int mode)
 				CRLF;
 			if (!(mode & P_SAVEATR))
 				attr(LIGHTGRAY);
-			lncntr = 0;
+			term->lncntr = 0;
 			return false;
 		}
 	}
@@ -353,7 +350,7 @@ int sbbs_t::getkeys(const char *keys, uint max, int mode)
 				attr(LIGHTGRAY);
 				CRLF;
 			}
-			lncntr = 0;
+			term->lncntr = 0;
 			return -1;
 		}
 		if (ch && !n && ((keys == NULL && !IS_DIGIT(ch)) || (strchr(str, ch)))) {  /* return character if in string */
@@ -374,7 +371,7 @@ int sbbs_t::getkeys(const char *keys, uint max, int mode)
 					}
 					if (c == BS || c == DEL) {
 						if (!(mode & K_NOECHO))
-							backspace();
+							term->backspace();
 						continue;
 					}
 				}
@@ -382,7 +379,7 @@ int sbbs_t::getkeys(const char *keys, uint max, int mode)
 					attr(LIGHTGRAY);
 					CRLF;
 				}
-				lncntr = 0;
+				term->lncntr = 0;
 			}
 			return ch;
 		}
@@ -391,14 +388,14 @@ int sbbs_t::getkeys(const char *keys, uint max, int mode)
 				attr(LIGHTGRAY);
 				CRLF;
 			}
-			lncntr = 0;
+			term->lncntr = 0;
 			if (n)
 				return i | 0x80000000L;    /* return number plus high bit */
 			return 0;
 		}
 		if ((ch == BS || ch == DEL) && n) {
 			if (!(mode & K_NOECHO))
-				backspace();
+				term->backspace();
 			i /= 10;
 			n--;
 		}
@@ -413,7 +410,7 @@ int sbbs_t::getkeys(const char *keys, uint max, int mode)
 					attr(LIGHTGRAY);
 					CRLF;
 				}
-				lncntr = 0;
+				term->lncntr = 0;
 				return i | 0x80000000L;
 			}
 		}
@@ -428,38 +425,37 @@ int sbbs_t::getkeys(const char *keys, uint max, int mode)
 bool sbbs_t::pause(bool set_abort)
 {
 	char   ch;
-	uint   tempattrs = curatr; /* was lclatr(-1) */
+	uint   tempattrs = term->curatr; /* was lclatr(-1) */
 	int    l = K_UPPER;
 	size_t len;
 
 	if ((sys_status & SS_ABORT) || pause_inside)
 		return false;
 	pause_inside = true;
-	lncntr = 0;
+	term->lncntr = 0;
 	if (online == ON_REMOTE)
 		rioctl(IOFI);
-	if (mouse_hotspots.first == NULL)
-		pause_hotspot = add_hotspot('\r');
+	term->pause_hotspot = term->add_pause_hotspot('\r');
 	bputs(text[Pause]);
-	len = bstrlen(text[Pause]);
+	len = term->bstrlen(text[Pause]);
 	if (sys_status & SS_USERON && !(useron.misc & (NOPAUSESPIN))
 	    && cfg.spinning_pause_prompt)
 		l |= K_SPIN;
 	ch = getkey(l);
-	if (pause_hotspot) {
-		clear_hotspots();
-		pause_hotspot = NULL;
+	if (term->pause_hotspot) {
+		term->clear_hotspots();
+		term->pause_hotspot = false;
 	}
 	bool aborted = (ch == no_key() || ch == quit_key() || (sys_status & SS_ABORT));
 	if (set_abort && aborted)
 		sys_status |= SS_ABORT;
 	if (text[Pause][0] != '@')
-		backspace(len);
+		term->backspace(len);
 	getnodedat(cfg.node_num, &thisnode);
 	nodesync();
 	attr(tempattrs);
 	if (ch == TERM_KEY_DOWN) // down arrow == display one more line
-		lncntr = rows - 2;
+		term->lncntr = term->rows - 2;
 	pause_inside = false;
 	return !aborted;
 }
diff --git a/src/sbbs3/getmsg.cpp b/src/sbbs3/getmsg.cpp
index b2fe8c56ea09a1db1380be1e0d7aaf4df5ba764c..06930d68ae348c523293d8b48aaa33e50c8692e6 100644
--- a/src/sbbs3/getmsg.cpp
+++ b/src/sbbs3/getmsg.cpp
@@ -189,13 +189,13 @@ void sbbs_t::show_msghdr(smb_t* smb, const smbmsg_t* msg, const char* subject, c
 		current_msg_to = to;
 
 	attr(LIGHTGRAY);
-	if (row != 0) {
+	if (term->row != 0) {
 		if (useron.misc & CLRSCRN)
 			outchar(FF);
 		else
 			CRLF;
 	}
-	msghdr_tos = (row == 0);
+	msghdr_tos = (term->row == 0);
 	if (!menu("msghdr", P_NOERROR)) {
 		bprintf(pmode, msghdr_text(msg, MsgSubj), current_msg_subj);
 		if (msg->tags && *msg->tags)
@@ -269,14 +269,14 @@ bool sbbs_t::show_msg(smb_t* smb, smbmsg_t* msg, int p_mode, post_t* post)
 		CRLF;
 
 	if (msg->hdr.type == SMB_MSG_TYPE_POLL && post != NULL && is_sub) {
-		char* answer;
-		int   longest_answer = 0;
+		char*    answer;
+		unsigned longest_answer = 0;
 
 		for (int i = 0; i < msg->total_hfields; i++) {
 			if (msg->hfield[i].type != SMB_POLL_ANSWER)
 				continue;
 			answer = (char*)msg->hfield_dat[i];
-			int len = strlen(answer);
+			size_t len = strlen(answer);
 			if (len > longest_answer)
 				longest_answer = len;
 		}
@@ -287,11 +287,11 @@ bool sbbs_t::show_msg(smb_t* smb, smbmsg_t* msg, int p_mode, post_t* post)
 			answer = (char*)msg->hfield_dat[i];
 			float pct = post->total_votes ? ((float)post->votes[answers] / post->total_votes) * 100.0F : 0.0F;
 			char  str[128];
-			int   width = longest_answer;
-			if (width < cols / 3)
-				width = cols / 3;
-			else if (width > cols - 20)
-				width = cols - 20;
+			unsigned width = longest_answer;
+			if (width < term->cols / 3)
+				width = term->cols / 3;
+			else if (width > term->cols - 20)
+				width = term->cols - 20;
 			bprintf(text[PollAnswerNumber], answers + 1);
 			bool results_visible = false;
 			if ((msg->hdr.auxattr & POLL_RESULTS_MASK) == POLL_RESULTS_OPEN)
@@ -337,7 +337,7 @@ bool sbbs_t::show_msg(smb_t* smb, smbmsg_t* msg, int p_mode, post_t* post)
 	truncsp(p);
 	SKIP_CRLF(p);
 	if (smb_msg_is_utf8(msg)) {
-		if (!term_supports(UTF8))
+		if (!(term->charset() == CHARSET_UTF8))
 			utf8_normalize_str(txt);
 		p_mode |= P_UTF8;
 	}
@@ -349,7 +349,7 @@ bool sbbs_t::show_msg(smb_t* smb, smbmsg_t* msg, int p_mode, post_t* post)
 		p_mode = P_NOATCODES;
 	putmsg(p, p_mode, msg->columns);
 	smb_freemsgtxt(txt);
-	if (column)
+	if (term->column)
 		CRLF;
 	if ((txt = smb_getmsgtxt(smb, msg, GETMSGTXT_TAIL_ONLY)) == NULL)
 		return false;
diff --git a/src/sbbs3/getnode.cpp b/src/sbbs3/getnode.cpp
index cf90bc5a9518a1c0f82ab8b37a1b71a040a224c6..2e5193deb61a1d58a95d23c28a0c549f5c9b5a4c 100644
--- a/src/sbbs3/getnode.cpp
+++ b/src/sbbs3/getnode.cpp
@@ -175,19 +175,19 @@ void sbbs_t::nodesync(bool clearline)
 	}
 
 	if (thisnode.misc & NODE_LCHAT) { // pulled into local chat with sysop
-		saveline();
+		term->saveline();
 		privchat(true);
-		restoreline();
+		term->restoreline();
 	}
 
 	if (thisnode.misc & NODE_FCHAT) { // forced into private chat
 		int n = getpagingnode(&cfg);
 		if (n) {
 			uint save_action = action;
-			saveline();
+			term->saveline();
 			privchat(true, n);
 			action = save_action;
-			restoreline();
+			term->restoreline();
 		}
 		if (getnodedat(cfg.node_num, &thisnode, true)) {
 			thisnode.action = action;
@@ -260,8 +260,8 @@ bool sbbs_t::getnmsg(bool clearline)
 	buf[length] = 0;
 
 	if (clearline)
-		this->clearline();
-	else if (column)
+		term->clearline();
+	else if (term->column)
 		CRLF;
 	putmsg(buf, P_NOATCODES);
 	free(buf);
@@ -343,9 +343,9 @@ bool sbbs_t::getsmsg(int usernumber, bool clearline)
 		return false;
 	getnodedat(cfg.node_num, &thisnode);
 	if (clearline)
-		this->clearline();
+		term->clearline();
 	else
-	if (column)
+	if (term->column)
 		CRLF;
 	putmsg(buf, P_NOATCODES);
 	free(buf);
diff --git a/src/sbbs3/getstr.cpp b/src/sbbs3/getstr.cpp
index fd0d10f7936088ad51e003be2c5ccbf9c397221d..0df5afce0c05ef3d3cb2550ebfa4a670deadde94 100644
--- a/src/sbbs3/getstr.cpp
+++ b/src/sbbs3/getstr.cpp
@@ -38,23 +38,21 @@ size_t sbbs_t::getstr(char *strout, size_t maxlen, int mode, const str_list_t hi
 	uchar  ch;
 	uint   atr;
 	int    hidx = -1;
-	int    org_column = column;
-	int    org_lbuflen = lbuflen;
+	int    org_column = term->column;
+	int    org_lbuflen = term->lbuflen;
 
-	int    term = term_supports();
 	console &= ~(CON_UPARROW | CON_DOWNARROW | CON_LEFTARROW | CON_RIGHTARROW | CON_BACKSPACE | CON_DELETELINE);
 	if (!(mode & K_WORDWRAP))
 		console &= ~CON_INSERT;
 	sys_status &= ~SS_ABORT;
-	if (!(mode & K_LINEWRAP) && cols >= TERM_COLS_MIN && !(mode & K_NOECHO) && !(console & CON_R_ECHOX)
-	    && column + (int)maxlen >= cols)    /* Don't allow the terminal to auto line-wrap */
-		maxlen = cols - column - 1;
-	if (mode & K_LINE && (term & (ANSI | PETSCII)) && !(mode & K_NOECHO)) {
+	if (!(mode & K_LINEWRAP) && term->cols >= TERM_COLS_MIN && !(mode & K_NOECHO) && !(console & CON_R_ECHOX)
+	    && term->column + (int)maxlen >= term->cols)    /* Don't allow the terminal to auto line-wrap */
+		maxlen = term->cols - term->column - 1;
+	if (mode & K_LINE && (term->can_highlight()) && !(mode & K_NOECHO)) {
 		attr(cfg.color[clr_inputline]);
 		for (i = 0; i < maxlen; i++)
-			outcom(' ');
-		cursor_left(maxlen);
-		column = org_column;
+			term_out(' ');
+		term->cursor_left(maxlen);
 	}
 	if (wordwrap[0]) {
 		SAFECOPY(str1, wordwrap);
@@ -71,29 +69,29 @@ size_t sbbs_t::getstr(char *strout, size_t maxlen, int mode, const str_list_t hi
 	atr = curatr;
 	if (!(mode & K_NOECHO)) {
 		if (mode & K_AUTODEL && str1[0]) {
-			i = (cfg.color[clr_inputline] & 0x77) << 4;
-			i |= (cfg.color[clr_inputline] & 0x77) >> 4;
+			i = (cfg.color[clr_inputline] & 0x07) << 4;
+			i |= (cfg.color[clr_inputline] & 0x70) >> 4;
 			attr(i);
 		}
 		bputs(str1, P_AUTO_UTF8);
 		if (mode & K_EDIT && !(mode & (K_LINE | K_AUTODEL)))
-			cleartoeol();  /* destroy to eol */
+			term->cleartoeol();  /* destroy to eol */
 	}
 
 	SAFECOPY(undo, str1);
-	i = l = bstrlen(str1, P_AUTO_UTF8);
+	i = l = term->bstrlen(str1, P_AUTO_UTF8);
 	if (mode & K_AUTODEL && str1[0] && !(mode & K_NOECHO)) {
 		ch = getkey(mode | K_GETSTR);
 		attr(atr);
 		if (IS_PRINTABLE(ch) || ch == DEL) {
 			for (i = 0; i < l; i++)
-				backspace();
+				term->backspace();
 			i = l = 0;
 		}
 		else {
 			for (i = 0; i < l; i++)
 				outchar(BS);
-			column += bputs(str1, P_AUTO_UTF8);
+			bputs(str1, P_AUTO_UTF8);
 			i = l;
 		}
 		if (ch != ' ' && ch != TAB)
@@ -105,12 +103,12 @@ size_t sbbs_t::getstr(char *strout, size_t maxlen, int mode, const str_list_t hi
 		if (i > l)
 			i = l;
 		if (l - i) {
-			cursor_left(l - i);
+			term->cursor_left(l - i);
 		}
 	}
 
 	if (console & CON_INSERT && !(mode & K_NOECHO))
-		insert_indicator();
+		term->insert_indicator();
 
 	while (!(sys_status & SS_ABORT) && online && input_thread_running) {
 		if (mode & K_LEFTEXIT
@@ -151,8 +149,8 @@ size_t sbbs_t::getstr(char *strout, size_t maxlen, int mode, const str_list_t hi
 						l++;
 					for (x = l; x > i; x--)
 						str1[x] = str1[x - 1];
-					column += rprintf("%.*s", (int)(l - i), str1 + i);
-					cursor_left(l - i);
+					rprintf("%.*s", (int)(l - i), str1 + i);
+					term->cursor_left(l - i);
 #if 0
 					if (i == maxlen - 1)
 						console &= ~CON_INSERT;
@@ -162,7 +160,7 @@ size_t sbbs_t::getstr(char *strout, size_t maxlen, int mode, const str_list_t hi
 				break;
 			case TERM_KEY_HOME: /* Ctrl-B Beginning of Line */
 				if (i && !(mode & K_NOECHO)) {
-					cursor_left(i);
+					term->cursor_left(i);
 					i = 0;
 				}
 				break;
@@ -177,7 +175,7 @@ size_t sbbs_t::getstr(char *strout, size_t maxlen, int mode, const str_list_t hi
 						outchar(' ');
 						x++;
 					}
-					cursor_left(x - i);   /* move cursor back */
+					term->cursor_left(x - i);   /* move cursor back */
 					z = i;
 					while (z < l - (x - i))  {             /* move chars in string */
 						outchar(str1[z] = str1[z + (x - i)]);
@@ -187,19 +185,19 @@ size_t sbbs_t::getstr(char *strout, size_t maxlen, int mode, const str_list_t hi
 						outchar(' ');
 						z++;
 					}
-					cursor_left(z - i);
+					term->cursor_left(z - i);
 					l -= x - i;                         /* l=new length */
 				}
 				break;
 			case TERM_KEY_END: /* Ctrl-E End of line */
-				if (term & (ANSI | PETSCII) && i < l) {
-					cursor_right(l - i);  /* move cursor to eol */
+				if (term->can_move() && i < l) {
+					term->cursor_right(l - i);  /* move cursor to eol */
 					i = l;
 				}
 				break;
 			case TERM_KEY_RIGHT: /* Ctrl-F move cursor forward */
-				if (i < l && term & (ANSI | PETSCII)) {
-					cursor_right();   /* move cursor right one */
+				if (i < l && term->can_move()) {
+					term->cursor_right();   /* move cursor right one */
 					i++;
 				}
 				break;
@@ -255,7 +253,7 @@ size_t sbbs_t::getstr(char *strout, size_t maxlen, int mode, const str_list_t hi
 				do {
 					i--;
 					l--;
-				} while ((term & UTF8) && (i > 0) && (str1[i] & 0x80) && (str1[i - 1] & 0x80));
+				} while ((term->charset() == CHARSET_UTF8) && (i > 0) && (str1[i] & 0x80) && (str1[i - 1] & 0x80));
 				if (i != l) {              /* Deleting char in middle of line */
 					outchar(BS);
 					z = i;
@@ -264,10 +262,10 @@ size_t sbbs_t::getstr(char *strout, size_t maxlen, int mode, const str_list_t hi
 						z++;
 					}
 					outchar(' ');        /* write over the last char */
-					cursor_left((l - i) + 1);
+					term->cursor_left((l - i) + 1);
 				}
 				else if (!(mode & K_NOECHO))
-					backspace();
+					term->backspace();
 				break;
 			case CTRL_I:    /* Ctrl-I/TAB */
 				if (history != NULL) {
@@ -279,10 +277,10 @@ size_t sbbs_t::getstr(char *strout, size_t maxlen, int mode, const str_list_t hi
 							hidx = hi;
 							SAFECOPY(str1, history[hi]);
 							while (i--)
-								backspace();
+								term->backspace();
 							i = l = strlen(str1);
 							rputs(str1);
-							cleartoeol();
+							term->cleartoeol();
 							break;
 						}
 					break;
@@ -323,7 +321,7 @@ size_t sbbs_t::getstr(char *strout, size_t maxlen, int mode, const str_list_t hi
 
 			case CTRL_L:    /* Ctrl-L   Center line (used to be Ctrl-V) */
 				str1[l] = 0;
-				l = bstrlen(str1);
+				l = term->bstrlen(str1);
 				if (!l)
 					break;
 				for (x = 0; x < (maxlen - l) / 2; x++)
@@ -348,13 +346,13 @@ size_t sbbs_t::getstr(char *strout, size_t maxlen, int mode, const str_list_t hi
 				return l;
 
 			case CTRL_N:    /* Ctrl-N Next word */
-				if (i < l && term & (ANSI | PETSCII)) {
+				if (i < l && term->can_move()) {
 					x = i;
 					while (str1[i] != ' ' && i < l)
 						i++;
 					while (str1[i] == ' ' && i < l)
 						i++;
-					cursor_right(i - x);
+					term->cursor_right(i - x);
 				}
 				break;
 			case CTRL_R:    /* Ctrl-R Redraw Line */
@@ -365,7 +363,7 @@ size_t sbbs_t::getstr(char *strout, size_t maxlen, int mode, const str_list_t hi
 				if (mode & K_NOECHO)
 					break;
 				console ^= CON_INSERT;
-				insert_indicator();
+				term->insert_indicator();
 				break;
 			case CTRL_W:    /* Ctrl-W   Delete word left */
 				if (i < l) {
@@ -387,27 +385,27 @@ size_t sbbs_t::getstr(char *strout, size_t maxlen, int mode, const str_list_t hi
 						outchar(' ');
 						z++;
 					}
-					cursor_left(z - i);               /* back to new x corridnant */
+					term->cursor_left(z - i);               /* back to new x corridnant */
 					l -= x - i;                         /* l=new length */
 				} else {
 					while (i && str1[i - 1] == ' ') {
 						i--;
 						l--;
 						if (!(mode & K_NOECHO))
-							backspace();
+							term->backspace();
 					}
 					while (i && str1[i - 1] != ' ') {
 						i--;
 						l--;
 						if (!(mode & K_NOECHO))
-							backspace();
+							term->backspace();
 					}
 				}
 				break;
 			case CTRL_Y:    /* Ctrl-Y   Delete to end of line */
 				if (i != l) {  /* if not at EOL */
 					if (!(mode & K_NOECHO))
-						cleartoeol();
+						term->cleartoeol();
 					l = i;
 					break;
 				}
@@ -416,10 +414,10 @@ size_t sbbs_t::getstr(char *strout, size_t maxlen, int mode, const str_list_t hi
 				if (mode & K_NOECHO)
 					l = 0;
 				else {
-					cursor_left(i);
-					cleartoeol();
+					term->cursor_left(i);
+					term->cleartoeol();
 					l = 0;
-					lbuflen = org_lbuflen;
+					term->lbuflen = org_lbuflen;
 				}
 				i = 0;
 				console |= CON_DELETELINE;
@@ -427,12 +425,12 @@ size_t sbbs_t::getstr(char *strout, size_t maxlen, int mode, const str_list_t hi
 			case CTRL_Z:    /* Undo */
 				if (!(mode & K_NOECHO)) {
 					while (i--)
-						backspace();
+						term->backspace();
 				}
 				SAFECOPY(str1, undo);
 				i = l = strlen(str1);
 				rputs(str1);
-				cleartoeol();
+				term->cleartoeol();
 				break;
 			case CTRL_BACKSLASH:    /* Ctrl-\ Previous word */
 				if (i && !(mode & K_NOECHO)) {
@@ -441,7 +439,7 @@ size_t sbbs_t::getstr(char *strout, size_t maxlen, int mode, const str_list_t hi
 						i--;
 					while (str1[i - 1] != ' ' && i)
 						i--;
-					cursor_left(x - i);
+					term->cursor_left(x - i);
 				}
 				break;
 			case TERM_KEY_LEFT:  /* Ctrl-]/Left Arrow  Reverse Cursor Movement */
@@ -451,7 +449,7 @@ size_t sbbs_t::getstr(char *strout, size_t maxlen, int mode, const str_list_t hi
 					break;
 				}
 				if (!(mode & K_NOECHO)) {
-					cursor_left();   /* move cursor left one */
+					term->cursor_left();   /* move cursor left one */
 					i--;
 				}
 				break;
@@ -467,10 +465,10 @@ size_t sbbs_t::getstr(char *strout, size_t maxlen, int mode, const str_list_t hi
 					else
 						SAFECOPY(str1, history[hidx]);
 					while (i--)
-						backspace();
+						term->backspace();
 					i = l = strlen(str1);
 					rputs(str1);
-					cleartoeol();
+					term->cleartoeol();
 					break;
 				}
 				break;
@@ -482,11 +480,11 @@ size_t sbbs_t::getstr(char *strout, size_t maxlen, int mode, const str_list_t hi
 					}
 					hidx++;
 					while (i--)
-						backspace();
+						term->backspace();
 					SAFECOPY(str1, history[hidx]);
 					i = l = strlen(str1);
 					rputs(str1);
-					cleartoeol();
+					term->cleartoeol();
 					break;
 				}
 				if (!(mode & K_EDIT))
@@ -513,8 +511,8 @@ size_t sbbs_t::getstr(char *strout, size_t maxlen, int mode, const str_list_t hi
 							i--;
 							l--;
 							if (!(mode & K_NOECHO))
-								backspace();
-						} while ((term & UTF8) && (i > 0) && (str1[i] & 0x80) && (str1[i - 1] & 0x80));
+								term->backspace();
+						} while ((term->charset() == CHARSET_UTF8) && (i > 0) && (str1[i] & 0x80) && (str1[i - 1] & 0x80));
 					}
 					break;
 				}
@@ -525,7 +523,7 @@ size_t sbbs_t::getstr(char *strout, size_t maxlen, int mode, const str_list_t hi
 					z++;
 				}
 				outchar(' ');        /* write over the last char */
-				cursor_left((l - i) + 1);
+				term->cursor_left((l - i) + 1);
 				break;
 			default:
 				if (mode & K_WORDWRAP && i == maxlen && ch >= ' ' && !(console & CON_INSERT)) {
@@ -555,7 +553,7 @@ size_t sbbs_t::getstr(char *strout, size_t maxlen, int mode, const str_list_t hi
 					wordwrap[z] = 0;
 					if (!(mode & K_NOECHO))
 						while (z--) {
-							backspace();
+							term->backspace();
 							i--;
 						}
 					strrev(wordwrap);
@@ -582,8 +580,8 @@ size_t sbbs_t::getstr(char *strout, size_t maxlen, int mode, const str_list_t hi
 							l++;
 						for (x = l; x > i; x--)
 							str1[x] = str1[x - 1];
-						column += rprintf("%.*s", (int)(l - i), str1 + i);
-						cursor_left(l - i);
+						rprintf("%.*s", (int)(l - i), str1 + i);
+						term->cursor_left(l - i);
 #if 0
 						if (i == maxlen - 1) {
 							bputs("  \b\b");
@@ -593,12 +591,12 @@ size_t sbbs_t::getstr(char *strout, size_t maxlen, int mode, const str_list_t hi
 					}
 					str1[i++] = ch;
 					if (!(mode & K_NOECHO)) {
-						if ((term & UTF8) && (ch & 0x80)) {
+						if ((term->charset() == CHARSET_UTF8) && (ch & 0x80)) {
 							if (i > l)
 								l = i;
 							str1[l] = 0;
 							if (utf8_str_is_valid(str1))
-								redrwstr(str1, column - org_column, l, P_UTF8);
+								redrwstr(str1, term->column - org_column, l, P_UTF8);
 						} else {
 							outchar(ch);
 						}
@@ -632,8 +630,8 @@ size_t sbbs_t::getstr(char *strout, size_t maxlen, int mode, const str_list_t hi
 		if (!(mode & K_MSG && sys_status & SS_ABORT)) {
 			CRLF;
 		} else
-			carriage_return();
-		lncntr = 0;
+			term->carriage_return();
+		term->lncntr = 0;
 	}
 	return l;
 }
@@ -657,27 +655,27 @@ int sbbs_t::getnum(uint max, uint dflt)
 			if (useron.misc & COLDKEYS)
 				ch = getkey(K_UPPER);
 			if (ch == BS || ch == DEL) {
-				backspace();
+				term->backspace();
 				continue;
 			}
 			CRLF;
-			lncntr = 0;
+			term->lncntr = 0;
 			return -1;
 		}
 		else if (sys_status & SS_ABORT) {
 			CRLF;
-			lncntr = 0;
+			term->lncntr = 0;
 			return -1;
 		}
 		else if (ch == CR) {
 			CRLF;
-			lncntr = 0;
+			term->lncntr = 0;
 			if (!n)
 				return dflt;
 			return i;
 		}
 		else if ((ch == BS || ch == DEL) && n) {
-			backspace();
+			term->backspace();
 			i /= 10;
 			n--;
 		}
@@ -688,33 +686,10 @@ int sbbs_t::getnum(uint max, uint dflt)
 			outchar(ch);
 			if (i * 10UL > max && !(useron.misc & COLDKEYS) && keybuf_level() < 1) {
 				CRLF;
-				lncntr = 0;
+				term->lncntr = 0;
 				return i;
 			}
 		}
 	}
 	return 0;
 }
-
-void sbbs_t::insert_indicator(void)
-{
-	if (term_supports(ANSI)) {
-		char str[32];
-		int  col = column;
-		auto row = this->row;
-		ansi_save();
-		ansi_gotoxy(cols, 1);
-		int  tmpatr;
-		if (console & CON_INSERT) {
-			putcom(ansi_attr(tmpatr = BLINK | BLACK | (LIGHTGRAY << 4), curatr, str, term_supports(COLOR)));
-			outcom('I');
-		} else {
-			putcom(ansi(tmpatr = ANSI_NORMAL));
-			outcom(' ');
-		}
-		putcom(ansi_attr(curatr, tmpatr, str, term_supports(COLOR)));
-		ansi_restore();
-		column = col;
-		this->row = row;
-	}
-}
diff --git a/src/sbbs3/inkey.cpp b/src/sbbs3/inkey.cpp
index 8fea4699f56ca4f977b423a313579e38ec34552c..807fc657fe8582e0694189d72194b2a0f44d182b 100644
--- a/src/sbbs3/inkey.cpp
+++ b/src/sbbs3/inkey.cpp
@@ -47,8 +47,7 @@ int sbbs_t::kbincom(unsigned int timeout)
 
 int sbbs_t::translate_input(int ch)
 {
-	int term = term_supports();
-	if (term & PETSCII) {
+	if (term->charset() == CHARSET_PETSCII) {
 		switch (ch) {
 			case PETSCII_HOME:
 				return TERM_KEY_HOME;
@@ -72,14 +71,21 @@ int sbbs_t::translate_input(int ch)
 		if (IS_ALPHA(ch))
 			ch ^= 0x20; /* Swap upper/lower case */
 	}
-	else if (term & SWAP_DELETE) {
-		switch (ch) {
-			case TERM_KEY_DELETE:
-				ch = '\b';
-				break;
-			case '\b':
-				ch = TERM_KEY_DELETE;
-				break;
+	else {
+		bool lwe = last_inkey_was_esc;
+		if (ch == ESC)
+			last_inkey_was_esc = true;
+		if (lwe && ch == '[')
+			autoterm |= ANSI;	// A CSI means ANSI.
+		if (term->supports(SWAP_DELETE)) {
+			switch (ch) {
+				case TERM_KEY_DELETE:
+					ch = '\b';
+					break;
+				case '\b':
+					ch = TERM_KEY_DELETE;
+					break;
+			}
 		}
 	}
 
@@ -109,7 +115,7 @@ int sbbs_t::inkey(int mode, unsigned int timeout)
 	if (ch == NOINP)
 		return no_input;
 
-	if (term_supports(NO_EXASCII))
+	if (term->charset() == CHARSET_ASCII)
 		ch &= 0x7f; // e.g. strip parity bit
 
 	getkey_last_activity = time(NULL);
@@ -123,7 +129,7 @@ int sbbs_t::inkey(int mode, unsigned int timeout)
 
 	/* Translate (not control character) input into CP437 */
 	if (!(mode & K_UTF8)) {
-		if ((ch & 0x80) && term_supports(UTF8)) {
+		if ((ch & 0x80) && (term->charset() == CHARSET_UTF8)) {
 			char   utf8[UTF8_MAX_LEN] = { (char)ch };
 			size_t len = utf8_decode_firstbyte(ch);
 			if (len < 2 || len > sizeof(utf8))
@@ -154,16 +160,16 @@ int sbbs_t::inkey(int mode, unsigned int timeout)
 
 char sbbs_t::handle_ctrlkey(char ch, int mode)
 {
-	char str[512];
 	char tmp[512];
-	int  i, j;
+	int  i;
 
 	if (ch == TERM_KEY_ABORT) {  /* Ctrl-C Abort */
 		sys_status |= SS_ABORT;
 		if (mode & K_SPIN) /* back space once if on spinning cursor */
-			backspace();
+			term->backspace();
 		return 0;
 	}
+
 	if (ch == CTRL_Z && !(mode & (K_MSG | K_GETSTR))
 	    && action != NODE_PCHT) {  /* Ctrl-Z toggle raw input mode */
 		if (hotkey_inside & (1 << ch))
@@ -171,7 +177,7 @@ char sbbs_t::handle_ctrlkey(char ch, int mode)
 		hotkey_inside |= (1 << ch);
 		if (mode & K_SPIN)
 			bputs("\b ");
-		saveline();
+		term->saveline();
 		attr(LIGHTGRAY);
 		CRLF;
 		bputs(text[RawMsgInputModeIsNow]);
@@ -182,8 +188,8 @@ char sbbs_t::handle_ctrlkey(char ch, int mode)
 		console ^= CON_RAW_IN;
 		CRLF;
 		CRLF;
-		restoreline();
-		lncntr = 0;
+		term->restoreline();
+		term->lncntr = 0;
 		hotkey_inside &= ~(1 << ch);
 		if (action != NODE_MAIN && action != NODE_XFER)
 			return CTRL_Z;
@@ -193,11 +199,6 @@ char sbbs_t::handle_ctrlkey(char ch, int mode)
 	if (console & CON_RAW_IN)   /* ignore ctrl-key commands if in raw mode */
 		return ch;
 
-#if 0   /* experimental removal to fix Tracker1's pause module problem with down-arrow */
-	if (ch == LF)              /* ignore LF's if not in raw mode */
-		return 0;
-#endif
-
 	/* Global hot key event */
 	if (sys_status & SS_USERON) {
 		for (i = 0; i < cfg.total_hotkeys; i++)
@@ -210,7 +211,7 @@ char sbbs_t::handle_ctrlkey(char ch, int mode)
 			if (mode & K_SPIN)
 				bputs("\b ");
 			if (!(sys_status & SS_SPLITP)) {
-				saveline();
+				term->saveline();
 				attr(LIGHTGRAY);
 				CRLF;
 			}
@@ -224,14 +225,17 @@ char sbbs_t::handle_ctrlkey(char ch, int mode)
 				external(cmdstr(cfg.hotkey[i]->cmd, nulstr, nulstr, tmp), 0);
 			if (!(sys_status & SS_SPLITP)) {
 				CRLF;
-				restoreline();
+				term->restoreline();
 			}
-			lncntr = 0;
+			term->lncntr = 0;
 			hotkey_inside &= ~(1 << ch);
 			return 0;
 		}
 	}
 
+	if (term->parse_input_sequence(ch, mode))
+		return ch;
+
 	switch (ch) {
 		case CTRL_O:    /* Ctrl-O toggles pause temporarily */
 			console ^= CON_PAUSEOFF;
@@ -245,7 +249,7 @@ char sbbs_t::handle_ctrlkey(char ch, int mode)
 			if (mode & K_SPIN)
 				bputs("\b ");
 			if (!(sys_status & SS_SPLITP)) {
-				saveline();
+				term->saveline();
 				attr(LIGHTGRAY);
 				CRLF;
 			}
@@ -254,9 +258,9 @@ char sbbs_t::handle_ctrlkey(char ch, int mode)
 			sync();
 			if (!(sys_status & SS_SPLITP)) {
 				CRLF;
-				restoreline();
+				term->restoreline();
 			}
-			lncntr = 0;
+			term->lncntr = 0;
 			hotkey_inside &= ~(1 << ch);
 			return 0;
 
@@ -269,7 +273,7 @@ char sbbs_t::handle_ctrlkey(char ch, int mode)
 			if (mode & K_SPIN)
 				bputs("\b ");
 			if (!(sys_status & SS_SPLITP)) {
-				saveline();
+				term->saveline();
 				attr(LIGHTGRAY);
 				CRLF;
 			}
@@ -277,9 +281,9 @@ char sbbs_t::handle_ctrlkey(char ch, int mode)
 			sync();
 			if (!(sys_status & SS_SPLITP)) {
 				CRLF;
-				restoreline();
+				term->restoreline();
 			}
-			lncntr = 0;
+			term->lncntr = 0;
 			hotkey_inside &= ~(1 << ch);
 			return 0;
 		case CTRL_T: /* Ctrl-T Time information */
@@ -292,7 +296,7 @@ char sbbs_t::handle_ctrlkey(char ch, int mode)
 			hotkey_inside |= (1 << ch);
 			if (mode & K_SPIN)
 				bputs("\b ");
-			saveline();
+			term->saveline();
 			attr(LIGHTGRAY);
 			now = time(NULL);
 			bprintf(text[TiLogon], timestr(logontime));
@@ -304,8 +308,8 @@ char sbbs_t::handle_ctrlkey(char ch, int mode)
 			if (sys_status & SS_EVENT)
 				bprintf(text[ReducedTime], timestr(event_time));
 			sync();
-			restoreline();
-			lncntr = 0;
+			term->restoreline();
+			term->lncntr = 0;
 			hotkey_inside &= ~(1 << ch);
 			return 0;
 		case CTRL_K:  /*  Ctrl-K Control key menu */
@@ -318,405 +322,18 @@ char sbbs_t::handle_ctrlkey(char ch, int mode)
 			hotkey_inside |= (1 << ch);
 			if (mode & K_SPIN)
 				bputs("\b ");
-			saveline();
+			term->saveline();
 			attr(LIGHTGRAY);
-			lncntr = 0;
+			term->lncntr = 0;
 			if (mode & K_GETSTR)
 				bputs(text[GetStrMenu]);
 			else
 				bputs(text[ControlKeyMenu]);
 			sync();
-			restoreline();
-			lncntr = 0;
+			term->restoreline();
+			term->lncntr = 0;
 			hotkey_inside &= ~(1 << ch);
 			return 0;
-		case ESC:
-			i = kbincom((mode & K_GETSTR) ? 3000:1000);
-			if (i == NOINP)        // timed-out waiting for '['
-				return ESC;
-			ch = i;
-			if (ch != '[') {
-				ungetkey(ch, /* insert: */ true);
-				return ESC;
-			}
-			i = j = 0;
-			autoterm |= ANSI;             /* <ESC>[x means they have ANSI */
-#if 0 // this seems like a "bad idea" {tm}
-			if (sys_status & SS_USERON && useron.misc & AUTOTERM && !(useron.misc & ANSI)
-			    && useron.number) {
-				useron.misc |= ANSI;
-				putuserrec(&cfg, useron.number, U_MISC, 8, ultoa(useron.misc, str, 16));
-			}
-#endif
-			while (i < 10 && j < 30) {       /* up to 3 seconds */
-				ch = kbincom(100);
-				if (ch == (NOINP & 0xff)) {
-					j++;
-					continue;
-				}
-				if (i == 0 && ch == 'M' && mouse_mode != MOUSE_MODE_OFF) {
-					str[i++] = ch;
-					int button = kbincom(100);
-					if (button == NOINP) {
-						lprintf(LOG_DEBUG, "Timeout waiting for mouse button value");
-						continue;
-					}
-					str[i++] = button;
-					ch = kbincom(100);
-					if (ch < '!') {
-						lprintf(LOG_DEBUG, "Unexpected mouse-button (0x%02X) tracking char: 0x%02X < '!'"
-						        , button, ch);
-						continue;
-					}
-					str[i++] = ch;
-					int x = ch - '!';
-					ch = kbincom(100);
-					if (ch < '!') {
-						lprintf(LOG_DEBUG, "Unexpected mouse-button (0x%02X) tracking char: 0x%02X < '!'"
-						        , button, ch);
-						continue;
-					}
-					str[i++] = ch;
-					int y = ch - '!';
-					lprintf(LOG_DEBUG, "X10 Mouse button-click (0x%02X) reported at: %u x %u", button, x, y);
-					if (button == 0x20) { // Left-click
-						list_node_t* node;
-						for (node = mouse_hotspots.first; node != NULL; node = node->next) {
-							struct mouse_hotspot* spot = (struct mouse_hotspot*)node->data;
-							if (spot->y == y && x >= spot->minx && x <= spot->maxx)
-								break;
-						}
-						if (node == NULL) {
-							for (node = mouse_hotspots.first; node != NULL; node = node->next) {
-								struct mouse_hotspot* spot = (struct mouse_hotspot*)node->data;
-								if (spot->hungry && spot->y == y && x >= spot->minx)
-									break;
-							}
-						}
-						if (node == NULL) {
-							for (node = mouse_hotspots.last; node != NULL; node = node->prev) {
-								struct mouse_hotspot* spot = (struct mouse_hotspot*)node->data;
-								if (spot->hungry && spot->y == y && x <= spot->minx)
-									break;
-							}
-						}
-						if (node != NULL) {
-							struct mouse_hotspot* spot = (struct mouse_hotspot*)node->data;
-	#ifdef _DEBUG
-							{
-								char dbg[128];
-								c_escape_str(spot->cmd, dbg, sizeof(dbg), /* Ctrl-only? */ true);
-								lprintf(LOG_DEBUG, "Stuffing hot spot command into keybuf: '%s'", dbg);
-							}
-	#endif
-							ungetkeys(spot->cmd);
-							if (pause_inside && pause_hotspot == NULL)
-								return handle_ctrlkey(TERM_KEY_ABORT, mode);
-							return 0;
-						}
-						if (pause_inside && y == rows - 1)
-							return '\r';
-					} else if (button == '`' && console & CON_MOUSE_SCROLL) {
-						return TERM_KEY_UP;
-					} else if (button == 'a' && console & CON_MOUSE_SCROLL) {
-						return TERM_KEY_DOWN;
-					}
-					if ((button != 0x23 && console & CON_MOUSE_CLK_PASSTHRU)
-					    || (button == 0x23 && console & CON_MOUSE_REL_PASSTHRU)) {
-						for (j = i; j > 0; j--)
-							ungetkey(str[j - 1], /* insert: */ true);
-						ungetkey('[', /* insert: */ true);
-						return ESC;
-					}
-					if (button == 0x22)  // Right-click
-						return handle_ctrlkey(TERM_KEY_ABORT, mode);
-					return 0;
-				}
-				if (i == 0 && ch == '<' && mouse_mode != MOUSE_MODE_OFF) {
-					while (i < (int)sizeof(str) - 1) {
-						int byte = kbincom(100);
-						if (byte == NOINP) {
-							lprintf(LOG_DEBUG, "Timeout waiting for mouse report character (%d)", i);
-							return 0;
-						}
-						str[i++] = byte;
-						if (IS_ALPHA(byte))
-							break;
-					}
-					str[i] = 0;
-					int button = -1, x = 0, y = 0;
-					if (sscanf(str, "%d;%d;%d%c", &button, &x, &y, &ch) != 4
-					    || button < 0 || x < 1 || y < 1 || toupper(ch) != 'M') {
-						lprintf(LOG_DEBUG, "Invalid SGR mouse report sequence: '%s'", str);
-						return 0;
-					}
-					--x;
-					--y;
-					lprintf(LOG_DEBUG, "SGR Mouse button (0x%02X) %s reported at: %u x %u"
-					        , button, ch == 'M' ? "PRESS" : "RELEASE", x, y);
-					if (button == 0 && ch == 'm') { // Left-button release
-						list_node_t* node;
-						for (node = mouse_hotspots.first; node != NULL; node = node->next) {
-							struct mouse_hotspot* spot = (struct mouse_hotspot*)node->data;
-							if (spot->y == y && x >= spot->minx && x <= spot->maxx)
-								break;
-						}
-						if (node == NULL) {
-							for (node = mouse_hotspots.first; node != NULL; node = node->next) {
-								struct mouse_hotspot* spot = (struct mouse_hotspot*)node->data;
-								if (spot->hungry && spot->y == y && x >= spot->minx)
-									break;
-							}
-						}
-						if (node == NULL) {
-							for (node = mouse_hotspots.last; node != NULL; node = node->prev) {
-								struct mouse_hotspot* spot = (struct mouse_hotspot*)node->data;
-								if (spot->hungry && spot->y == y && x <= spot->minx)
-									break;
-							}
-						}
-						if (node != NULL) {
-							struct mouse_hotspot* spot = (struct mouse_hotspot*)node->data;
-	#ifdef _DEBUG
-							{
-								char dbg[128];
-								c_escape_str(spot->cmd, dbg, sizeof(dbg), /* Ctrl-only? */ true);
-								lprintf(LOG_DEBUG, "Stuffing hot spot command into keybuf: '%s'", dbg);
-							}
-	#endif
-							ungetkeys(spot->cmd);
-							if (pause_inside && pause_hotspot == NULL)
-								return handle_ctrlkey(TERM_KEY_ABORT, mode);
-							return 0;
-						}
-						if (pause_inside && y == rows - 1)
-							return '\r';
-					} else if (button == 0x40 && console & CON_MOUSE_SCROLL) {
-						return TERM_KEY_UP;
-					} else if (button == 0x41 && console & CON_MOUSE_SCROLL) {
-						return TERM_KEY_DOWN;
-					}
-					if ((ch == 'M' && console & CON_MOUSE_CLK_PASSTHRU)
-					    || (ch == 'm' && console & CON_MOUSE_REL_PASSTHRU)) {
-						lprintf(LOG_DEBUG, "Passing-through SGR mouse report: 'ESC[<%s'", str);
-						for (j = i; j > 0; j--)
-							ungetkey(str[j - 1], /* insert: */ true);
-						ungetkey('<', /* insert: */ true);
-						ungetkey('[', /* insert: */ true);
-						return ESC;
-					}
-					if (ch == 'M' && button == 2)  // Right-click
-						return handle_ctrlkey(TERM_KEY_ABORT, mode);
-	#ifdef _DEBUG
-					lprintf(LOG_DEBUG, "Eating SGR mouse report: 'ESC[<%s'", str);
-	#endif
-					return 0;
-				}
-				if (ch != ';' && !IS_DIGIT(ch) && ch != 'R') {    /* other ANSI */
-					str[i] = 0;
-					switch (ch) {
-						case 'A':
-							return TERM_KEY_UP;
-						case 'B':
-							return TERM_KEY_DOWN;
-						case 'C':
-							return TERM_KEY_RIGHT;
-						case 'D':
-							return TERM_KEY_LEFT;
-						case 'H':   /* ANSI:  home cursor */
-							return TERM_KEY_HOME;
-						case 'V':
-							return TERM_KEY_PAGEUP;
-						case 'U':
-							return TERM_KEY_PAGEDN;
-						case 'F':   /* Xterm: cursor preceding line */
-						case 'K':   /* ANSI:  clear-to-end-of-line */
-							return TERM_KEY_END;
-						case '@':   /* ANSI/ECMA-048 INSERT */
-							return TERM_KEY_INSERT;
-						case '~':   /* VT-220 (XP telnet.exe) */
-							switch (atoi(str)) {
-								case 1:
-									return TERM_KEY_HOME;
-								case 2:
-									return TERM_KEY_INSERT;
-								case 3:
-									return TERM_KEY_DELETE;
-								case 4:
-									return TERM_KEY_END;
-								case 5:
-									return TERM_KEY_PAGEUP;
-								case 6:
-									return TERM_KEY_PAGEDN;
-							}
-							break;
-					}
-					ungetkey(ch, /* insert: */ true);
-					for (j = i; j > 0; j--)
-						ungetkey(str[j - 1], /* insert: */ true);
-					ungetkey('[', /* insert: */ true);
-					return ESC;
-				}
-				if (ch == 'R') {       /* cursor position report */
-					if (mode & K_ANSI_CPR && i) {  /* auto-detect rows */
-						int x, y;
-						str[i] = 0;
-						if (sscanf(str, "%u;%u", &y, &x) == 2) {
-							lprintf(LOG_DEBUG, "received ANSI cursor position report: %ux%u"
-							        , x, y);
-							/* Sanity check the coordinates in the response: */
-							if (useron.cols == TERM_COLS_AUTO && x >= TERM_COLS_MIN && x <= TERM_COLS_MAX)
-								cols = x;
-							if (useron.rows == TERM_ROWS_AUTO && y >= TERM_ROWS_MIN && y <= TERM_ROWS_MAX)
-								rows = y;
-							if (useron.cols == TERM_COLS_AUTO || useron.rows == TERM_ROWS_AUTO)
-								update_nodeterm();
-						}
-					}
-					return 0;
-				}
-				str[i++] = ch;
-			}
-
-			for (j = i; j > 0; j--)
-				ungetkey(str[j - 1], /* insert: */ true);
-			ungetkey('[', /* insert: */ true);
-			return ESC;
 	}
 	return ch;
 }
-
-void sbbs_t::set_mouse(int flags)
-{
-	int term = term_supports();
-	if ((term & ANSI) && ((term & MOUSE) || flags == MOUSE_MODE_OFF)) {
-		int mode = mouse_mode & ~flags;
-		if (mode & MOUSE_MODE_X10)
-			ansi_mouse(ANSI_MOUSE_X10, false);
-		if (mode & MOUSE_MODE_NORM)
-			ansi_mouse(ANSI_MOUSE_NORM, false);
-		if (mode & MOUSE_MODE_BTN)
-			ansi_mouse(ANSI_MOUSE_BTN, false);
-		if (mode & MOUSE_MODE_ANY)
-			ansi_mouse(ANSI_MOUSE_ANY, false);
-		if (mode & MOUSE_MODE_EXT)
-			ansi_mouse(ANSI_MOUSE_EXT, false);
-
-		mode = flags & ~mouse_mode;
-		if (mode & MOUSE_MODE_X10)
-			ansi_mouse(ANSI_MOUSE_X10, true);
-		if (mode & MOUSE_MODE_NORM)
-			ansi_mouse(ANSI_MOUSE_NORM, true);
-		if (mode & MOUSE_MODE_BTN)
-			ansi_mouse(ANSI_MOUSE_BTN, true);
-		if (mode & MOUSE_MODE_ANY)
-			ansi_mouse(ANSI_MOUSE_ANY, true);
-		if (mode & MOUSE_MODE_EXT)
-			ansi_mouse(ANSI_MOUSE_EXT, true);
-
-		if (mouse_mode != flags) {
-#if 0
-			lprintf(LOG_DEBUG, "New mouse mode: %X (was: %X)", flags, mouse_mode);
-#endif
-			mouse_mode = flags;
-		}
-	}
-}
-
-struct mouse_hotspot* sbbs_t::add_hotspot(struct mouse_hotspot* spot)
-{
-	if (!(cfg.sys_misc & SM_MOUSE_HOT) || !term_supports(MOUSE))
-		return NULL;
-	if (spot->y < 0)
-		spot->y = row;
-	if (spot->minx < 0)
-		spot->minx = column;
-	if (spot->maxx < 0)
-		spot->maxx = cols - 1;
-#if 0 //def _DEBUG
-	char         dbg[128];
-	lprintf(LOG_DEBUG, "Adding mouse hot spot %ld-%ld x %ld = '%s'"
-	        , spot->minx, spot->maxx, spot->y, c_escape_str(spot->cmd, dbg, sizeof(dbg), /* Ctrl-only? */ true));
-#endif
-	list_node_t* node = listInsertNodeData(&mouse_hotspots, spot, sizeof(*spot));
-	if (node == NULL)
-		return NULL;
-	set_mouse(MOUSE_MODE_ON);
-	return (struct mouse_hotspot*)node->data;
-}
-
-void sbbs_t::clear_hotspots(void)
-{
-	int spots = listCountNodes(&mouse_hotspots);
-	if (spots) {
-#if 0 //def _DEBUG
-		lprintf(LOG_DEBUG, "Clearing %ld mouse hot spots", spots);
-#endif
-		listFreeNodes(&mouse_hotspots);
-		if (!(console & CON_MOUSE_SCROLL))
-			set_mouse(MOUSE_MODE_OFF);
-	}
-}
-
-void sbbs_t::scroll_hotspots(int count)
-{
-	int spots = 0;
-	int remain = 0;
-	for (list_node_t* node = mouse_hotspots.first; node != NULL; node = node->next) {
-		struct mouse_hotspot* spot = (struct mouse_hotspot*)node->data;
-		spot->y -= count;
-		spots++;
-		if (spot->y >= 0)
-			remain++;
-	}
-#ifdef _DEBUG
-	if (spots)
-		lprintf(LOG_DEBUG, "Scrolled %d mouse hot-spots %d rows (%d remain)", spots, count, remain);
-#endif
-	if (remain < 1)
-		clear_hotspots();
-}
-
-struct mouse_hotspot* sbbs_t::add_hotspot(char cmd, bool hungry, int minx, int maxx, int y)
-{
-	struct mouse_hotspot spot = {};
-	spot.cmd[0] = cmd;
-	spot.minx = minx < 0 ? column : minx;
-	spot.maxx = maxx < 0 ? column : maxx;
-	spot.y = y;
-	spot.hungry = hungry;
-	return add_hotspot(&spot);
-}
-
-struct mouse_hotspot* sbbs_t::add_hotspot(int num, bool hungry, int minx, int maxx, int y)
-{
-	struct mouse_hotspot spot = {};
-	SAFEPRINTF(spot.cmd, "%d\r", num);
-	spot.minx = minx;
-	spot.maxx = maxx;
-	spot.y = y;
-	spot.hungry = hungry;
-	return add_hotspot(&spot);
-}
-
-struct mouse_hotspot* sbbs_t::add_hotspot(uint num, bool hungry, int minx, int maxx, int y)
-{
-	struct mouse_hotspot spot = {};
-	SAFEPRINTF(spot.cmd, "%u\r", num);
-	spot.minx = minx;
-	spot.maxx = maxx;
-	spot.y = y;
-	spot.hungry = hungry;
-	return add_hotspot(&spot);
-}
-
-struct mouse_hotspot* sbbs_t::add_hotspot(const char* cmd, bool hungry, int minx, int maxx, int y)
-{
-	struct mouse_hotspot spot = {};
-	SAFECOPY(spot.cmd, cmd);
-	spot.minx = minx;
-	spot.maxx = maxx;
-	spot.y = y;
-	spot.hungry = hungry;
-	return add_hotspot(&spot);
-}
diff --git a/src/sbbs3/js_console.cpp b/src/sbbs3/js_console.cpp
index e54b5e37caf612f909a4c4930a8e3f77fba861cb..ff2f4d684925640fede98fff2cc37e6381dad28c 100644
--- a/src/sbbs3/js_console.cpp
+++ b/src/sbbs3/js_console.cpp
@@ -99,13 +99,13 @@ static JSBool js_console_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp)
 			val = sbbs->mouse_mode;
 			break;
 		case CON_PROP_LNCNTR:
-			val = sbbs->lncntr;
+			val = sbbs->term->lncntr;
 			break;
 		case CON_PROP_COLUMN:
-			val = sbbs->column;
+			val = sbbs->term->column;
 			break;
 		case CON_PROP_LASTLINELEN:
-			val = sbbs->lastlinelen;
+			val = sbbs->term->lastcrcol;
 			break;
 		case CON_PROP_LINE_DELAY:
 			val = sbbs->line_delay;
@@ -114,19 +114,19 @@ static JSBool js_console_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp)
 			val = sbbs->curatr;
 			break;
 		case CON_PROP_TOS:
-			val = sbbs->row == 0;
+			val = sbbs->term->row == 0;
 			break;
 		case CON_PROP_ROW:
-			val = sbbs->row;
+			val = sbbs->term->row;
 			break;
 		case CON_PROP_ROWS:
-			val = sbbs->rows;
+			val = sbbs->term->rows;
 			break;
 		case CON_PROP_COLUMNS:
-			val = sbbs->cols;
+			val = sbbs->term->cols;
 			break;
 		case CON_PROP_TABSTOP:
-			val = sbbs->tabstop;
+			val = sbbs->term->tabstop;
 			break;
 		case CON_PROP_AUTOTERM:
 			val = sbbs->autoterm;
@@ -140,14 +140,14 @@ static JSBool js_console_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp)
 				return JS_FALSE;
 			break;
 		case CON_PROP_CHARSET:
-			if ((js_str = JS_NewStringCopyZ(cx, sbbs->term_charset())) == NULL)
+			if ((js_str = JS_NewStringCopyZ(cx, sbbs->term->charset_str())) == NULL)
 				return JS_FALSE;
 			break;
 		case CON_PROP_UNICODE_ZEROWIDTH:
 			val = sbbs->unicode_zerowidth;
 			break;
 		case CON_PROP_CTERM_VERSION:
-			val = sbbs->cterm_version;
+			val = sbbs->term->cterm_version;
 			break;
 		case CON_PROP_MAX_GETKEY_INACTIVITY:
 			val = sbbs->cfg.max_getkey_inactivity;
@@ -200,7 +200,7 @@ static JSBool js_console_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp)
 			val = RingBufFree(&sbbs->outbuf);
 			break;
 		case CON_PROP_OUTPUT_RATE:
-			val = sbbs->cur_output_rate;
+			val = sbbs->term->cur_output_rate;
 			break;
 		case CON_PROP_KEYBUF_LEVEL:
 			val = sbbs->keybuf_level();
@@ -278,16 +278,16 @@ static JSBool js_console_set(JSContext *cx, JSObject *obj, jsid id, JSBool stric
 		case CON_PROP_MOUSE_MODE:
 			if (*vp == JSVAL_TRUE)
 				val = MOUSE_MODE_ON;
-			sbbs->set_mouse(val);
+			sbbs->term->set_mouse(val);
 			break;
 		case CON_PROP_LNCNTR:
-			sbbs->lncntr = val;
+			sbbs->term->lncntr = val;
 			break;
 		case CON_PROP_COLUMN:
-			sbbs->column = val;
+			sbbs->term->column = val;
 			break;
 		case CON_PROP_LASTLINELEN:
-			sbbs->lastlinelen = val;
+			sbbs->term->lastcrcol = val;
 			break;
 		case CON_PROP_LINE_DELAY:
 			sbbs->line_delay = val;
@@ -306,18 +306,18 @@ static JSBool js_console_set(JSContext *cx, JSObject *obj, jsid id, JSBool stric
 			break;
 		case CON_PROP_ROW:
 			if (val >= 0 && val < TERM_ROWS_MAX)
-				sbbs->row = val;
+				sbbs->term->row = val;
 			break;
 		case CON_PROP_ROWS:
 			if (val >= TERM_ROWS_MIN && val <= TERM_ROWS_MAX)
-				sbbs->rows = val;
+				sbbs->term->rows = val;
 			break;
 		case CON_PROP_COLUMNS:
 			if (val >= TERM_COLS_MIN && val <= TERM_COLS_MAX)
-				sbbs->cols = val;
+				sbbs->term->cols = val;
 			break;
 		case CON_PROP_TABSTOP:
-			sbbs->tabstop = val;
+			sbbs->term->tabstop = val;
 			break;
 		case CON_PROP_AUTOTERM:
 			sbbs->autoterm = val;
@@ -333,7 +333,7 @@ static JSBool js_console_set(JSContext *cx, JSObject *obj, jsid id, JSBool stric
 			free(sval);
 			break;
 		case CON_PROP_CTERM_VERSION:
-			sbbs->cterm_version = val;
+			sbbs->term->cterm_version = val;
 			break;
 		case CON_PROP_MAX_GETKEY_INACTIVITY:
 			sbbs->cfg.max_getkey_inactivity = (uint16_t)val;
@@ -385,7 +385,7 @@ static JSBool js_console_set(JSContext *cx, JSObject *obj, jsid id, JSBool stric
 			sbbs->cfg.ctrlkey_passthru = val;
 			break;
 		case CON_PROP_OUTPUT_RATE:
-			sbbs->set_output_rate((enum sbbs_t::output_rate)val);
+			sbbs->term->set_output_rate((enum output_rate)val);
 			break;
 
 		default:
@@ -423,7 +423,7 @@ static jsSyncPropertySpec js_console_properties[] = {
 	{   "getkey_inactivity_warning", CON_PROP_GETKEY_INACTIVITY_WARN, JSPROP_ENUMERATE | JSPROP_READONLY, 32002},
 	{   "inactivity_warning", CON_PROP_GETKEY_INACTIVITY_WARN, 0, 32002},
 	{   "last_getkey_activity", CON_PROP_LAST_GETKEY_ACTIVITY, CON_PROP_FLAGS, 320},
-	{   "timeout", CON_PROP_LAST_GETKEY_ACTIVITY, 0, 310},                              // alias
+	{   "timeout", CON_PROP_LAST_GETKEY_ACTIVITY, 0, 310},                               // alias
 	{   "max_socket_inactivity", CON_PROP_MAX_SOCKET_INACTIVITY, CON_PROP_FLAGS, 320},
 	{   "timeleft_warning", CON_PROP_TIMELEFT_WARN, CON_PROP_FLAGS, 310},
 	{   "aborted", CON_PROP_ABORTED, CON_PROP_FLAGS, 310},
@@ -458,7 +458,8 @@ static const char*        con_prop_desc[] = {
 	, "Current 0-based line counter (used for automatic screen pause)"
 	, "Current 0-based row counter"
 	, "Current 0-based column counter (used to auto-increment <i>line_counter</i> when screen wraps)"
-	, "Length of last line sent to terminal (before a carriage-return or line-wrap)"
+	, "Column the cursor was on when last CR was sent to terminal or the line wrapped"
+	, "Obsolete alias for last_cr_column"
 	, "Duration of delay (in milliseconds) before each line-feed character is sent to the terminal"
 	, "Current display attributes (set with number or string value)"
 	, "<tt>true</tt> if the terminal cursor is already at the top of the screen - <small>READ ONLY</small>"
@@ -679,7 +680,7 @@ js_add_hotspot(JSContext *cx, uintN argc, jsval *arglist)
 	JSSTRING_TO_MSTRING(cx, js_str, p, NULL);
 	if (p == NULL)
 		return JS_FALSE;
-	sbbs->add_hotspot(p, hungry, min_x, max_x, y);
+	sbbs->term->add_hotspot(p, hungry, min_x, max_x, y);
 	free(p);
 	return JS_TRUE;
 }
@@ -697,7 +698,7 @@ static JSBool js_scroll_hotspots(JSContext *cx, uintN argc, jsval *arglist)
 	int32 rows = 1;
 	if (argc > 0 && !JS_ValueToInt32(cx, argv[0], &rows))
 		return JS_FALSE;
-	sbbs->scroll_hotspots(rows);
+	sbbs->term->scroll_hotspots(rows);
 	return JS_TRUE;
 }
 
@@ -710,7 +711,7 @@ static JSBool js_clear_hotspots(JSContext *cx, uintN argc, jsval *arglist)
 	if ((sbbs = (sbbs_t*)js_GetClassPrivate(cx, JS_THIS_OBJECT(cx, arglist), &js_console_class)) == NULL)
 		return JS_FALSE;
 
-	sbbs->clear_hotspots();
+	sbbs->term->clear_hotspots();
 	return JS_TRUE;
 }
 
@@ -1185,7 +1186,7 @@ js_clear(JSContext *cx, uintN argc, jsval *arglist)
 	if (autopause)
 		sbbs->CLS;
 	else
-		sbbs->clearscreen(sbbs->term_supports());
+		sbbs->term->clearscreen();
 	JS_RESUMEREQUEST(cx, rc);
 	return JS_TRUE;
 }
@@ -1208,7 +1209,7 @@ js_clearline(JSContext *cx, uintN argc, jsval *arglist)
 	}
 
 	rc = JS_SUSPENDREQUEST(cx);
-	sbbs->clearline();
+	sbbs->term->clearline();
 	JS_RESUMEREQUEST(cx, rc);
 	return JS_TRUE;
 }
@@ -1231,7 +1232,7 @@ js_cleartoeol(JSContext *cx, uintN argc, jsval *arglist)
 	}
 
 	rc = JS_SUSPENDREQUEST(cx);
-	sbbs->cleartoeol();
+	sbbs->term->cleartoeol();
 	JS_RESUMEREQUEST(cx, rc);
 	return JS_TRUE;
 }
@@ -1254,7 +1255,7 @@ js_cleartoeos(JSContext *cx, uintN argc, jsval *arglist)
 	}
 
 	rc = JS_SUSPENDREQUEST(cx);
-	sbbs->cleartoeos();
+	sbbs->term->cleartoeos();
 	JS_RESUMEREQUEST(cx, rc);
 	return JS_TRUE;
 }
@@ -1278,7 +1279,7 @@ js_newline(JSContext *cx, uintN argc, jsval *arglist)
 	}
 
 	rc = JS_SUSPENDREQUEST(cx);
-	sbbs->newline(count);
+	sbbs->term->newline(count);
 	JS_RESUMEREQUEST(cx, rc);
 	return JS_TRUE;
 }
@@ -1295,7 +1296,7 @@ js_cond_newline(JSContext *cx, uintN argc, jsval *arglist)
 	JS_SET_RVAL(cx, arglist, JSVAL_VOID);
 
 	rc = JS_SUSPENDREQUEST(cx);
-	sbbs->cond_newline();
+	sbbs->term->cond_newline();
 	JS_RESUMEREQUEST(cx, rc);
 	return JS_TRUE;
 }
@@ -1312,7 +1313,7 @@ js_cond_blankline(JSContext *cx, uintN argc, jsval *arglist)
 	JS_SET_RVAL(cx, arglist, JSVAL_VOID);
 
 	rc = JS_SUSPENDREQUEST(cx);
-	sbbs->cond_blankline();
+	sbbs->term->cond_blankline();
 	JS_RESUMEREQUEST(cx, rc);
 	return JS_TRUE;
 }
@@ -1329,7 +1330,7 @@ js_cond_contline(JSContext *cx, uintN argc, jsval *arglist)
 	JS_SET_RVAL(cx, arglist, JSVAL_VOID);
 
 	rc = JS_SUSPENDREQUEST(cx);
-	sbbs->cond_contline();
+	sbbs->term->cond_contline();
 	JS_RESUMEREQUEST(cx, rc);
 	return JS_TRUE;
 }
@@ -1451,7 +1452,7 @@ js_strlen(JSContext *cx, uintN argc, jsval *arglist)
 	if (cstr == NULL)
 		return JS_FALSE;
 	rc = JS_SUSPENDREQUEST(cx);
-	JS_SET_RVAL(cx, arglist, INT_TO_JSVAL(sbbs->bstrlen(cstr, pmode)));
+	JS_SET_RVAL(cx, arglist, INT_TO_JSVAL(sbbs->term->bstrlen(cstr, pmode)));
 	free(cstr);
 	JS_RESUMEREQUEST(cx, rc);
 	return JS_TRUE;
@@ -1841,7 +1842,7 @@ js_center(JSContext *cx, uintN argc, jsval *arglist)
 	if (cstr == NULL)
 		return JS_FALSE;
 	rc = JS_SUSPENDREQUEST(cx);
-	sbbs->center(cstr, cols);
+	sbbs->term->center(cstr, cols);
 	free(cstr);
 	JS_RESUMEREQUEST(cx, rc);
 	return JS_TRUE;
@@ -1883,7 +1884,7 @@ js_saveline(JSContext *cx, uintN argc, jsval *arglist)
 	if ((sbbs = (sbbs_t*)js_GetClassPrivate(cx, JS_THIS_OBJECT(cx, arglist), &js_console_class)) == NULL)
 		return JS_FALSE;
 
-	JS_SET_RVAL(cx, arglist, BOOLEAN_TO_JSVAL(sbbs->saveline()));
+	JS_SET_RVAL(cx, arglist, BOOLEAN_TO_JSVAL(sbbs->term->saveline()));
 	return JS_TRUE;
 }
 
@@ -1897,7 +1898,7 @@ js_restoreline(JSContext *cx, uintN argc, jsval *arglist)
 		return JS_FALSE;
 
 	rc = JS_SUSPENDREQUEST(cx);
-	JS_SET_RVAL(cx, arglist, BOOLEAN_TO_JSVAL(sbbs->restoreline()));
+	JS_SET_RVAL(cx, arglist, BOOLEAN_TO_JSVAL(sbbs->term->restoreline()));
 	JS_RESUMEREQUEST(cx, rc);
 	return JS_TRUE;
 }
@@ -1921,14 +1922,15 @@ js_ansi(JSContext *cx, uintN argc, jsval *arglist)
 	}
 	if (argc > 1) {
 		int32 curattr = 0;
-		char  buf[16];
+		char  buf[128];
 
 		if (!JS_ValueToInt32(cx, argv[1], &curattr))
 			return JS_FALSE;
-		if ((js_str = JS_NewStringCopyZ(cx, sbbs->ansi(attr, curattr, buf))) == NULL)
+		// TODO: A way to use term->curattr here...
+		if ((js_str = JS_NewStringCopyZ(cx, sbbs->term->attrstr(attr, curattr, buf, sizeof(buf)))) == NULL)
 			return JS_FALSE;
 	} else {
-		if ((js_str = JS_NewStringCopyZ(cx, sbbs->ansi(attr))) == NULL)
+		if ((js_str = JS_NewStringCopyZ(cx, sbbs->term->attrstr(attr))) == NULL)
 			return JS_FALSE;
 	}
 
@@ -1946,7 +1948,7 @@ js_pushxy(JSContext *cx, uintN argc, jsval *arglist)
 		return JS_FALSE;
 
 	rc = JS_SUSPENDREQUEST(cx);
-	JS_SET_RVAL(cx, arglist, BOOLEAN_TO_JSVAL(sbbs->ansi_save()));
+	JS_SET_RVAL(cx, arglist, BOOLEAN_TO_JSVAL(sbbs->term->save_cursor_pos()));
 	JS_RESUMEREQUEST(cx, rc);
 	return JS_TRUE;
 }
@@ -1961,7 +1963,7 @@ js_popxy(JSContext *cx, uintN argc, jsval *arglist)
 		return JS_FALSE;
 
 	rc = JS_SUSPENDREQUEST(cx);
-	JS_SET_RVAL(cx, arglist, BOOLEAN_TO_JSVAL(sbbs->ansi_restore()));
+	JS_SET_RVAL(cx, arglist, BOOLEAN_TO_JSVAL(sbbs->term->restore_cursor_pos()));
 	JS_RESUMEREQUEST(cx, rc);
 	return JS_TRUE;
 }
@@ -1999,7 +2001,7 @@ js_gotoxy(JSContext *cx, uintN argc, jsval *arglist)
 	}
 
 	rc = JS_SUSPENDREQUEST(cx);
-	JS_SET_RVAL(cx, arglist, BOOLEAN_TO_JSVAL(sbbs->cursor_xy(x, y)));
+	JS_SET_RVAL(cx, arglist, BOOLEAN_TO_JSVAL(sbbs->term->gotoxy(x, y)));
 	JS_RESUMEREQUEST(cx, rc);
 	return JS_TRUE;
 }
@@ -2010,7 +2012,7 @@ js_getxy(JSContext *cx, uintN argc, jsval *arglist)
 {
 	JSObject * obj = JS_THIS_OBJECT(cx, arglist);
 	sbbs_t*    sbbs;
-	int        x, y;
+	unsigned   x, y;
 	JSObject*  screen;
 	jsrefcount rc;
 
@@ -2020,7 +2022,7 @@ js_getxy(JSContext *cx, uintN argc, jsval *arglist)
 	JS_SET_RVAL(cx, arglist, JSVAL_FALSE);
 
 	rc = JS_SUSPENDREQUEST(cx);
-	bool result = sbbs->cursor_getxy(&x, &y);
+	bool result = sbbs->term->getxy(&x, &y);
 	JS_RESUMEREQUEST(cx, rc);
 
 	if (result == true) {
@@ -2049,7 +2051,7 @@ js_cursor_home(JSContext *cx, uintN argc, jsval *arglist)
 	JS_SET_RVAL(cx, arglist, JSVAL_VOID);
 
 	rc = JS_SUSPENDREQUEST(cx);
-	sbbs->cursor_home();
+	sbbs->term->cursor_home();
 	JS_RESUMEREQUEST(cx, rc);
 	return JS_TRUE;
 }
@@ -2072,7 +2074,7 @@ js_cursor_up(JSContext *cx, uintN argc, jsval *arglist)
 			return JS_FALSE;
 	}
 	rc = JS_SUSPENDREQUEST(cx);
-	sbbs->cursor_up(val);
+	sbbs->term->cursor_up(val);
 	JS_RESUMEREQUEST(cx, rc);
 	return JS_TRUE;
 }
@@ -2095,7 +2097,7 @@ js_cursor_down(JSContext *cx, uintN argc, jsval *arglist)
 			return JS_FALSE;
 	}
 	rc = JS_SUSPENDREQUEST(cx);
-	sbbs->cursor_down(val);
+	sbbs->term->cursor_down(val);
 	JS_RESUMEREQUEST(cx, rc);
 	return JS_TRUE;
 }
@@ -2118,7 +2120,7 @@ js_cursor_right(JSContext *cx, uintN argc, jsval *arglist)
 			return JS_FALSE;
 	}
 	rc = JS_SUSPENDREQUEST(cx);
-	sbbs->cursor_right(val);
+	sbbs->term->cursor_right(val);
 	JS_RESUMEREQUEST(cx, rc);
 	return JS_TRUE;
 }
@@ -2141,7 +2143,7 @@ js_cursor_left(JSContext *cx, uintN argc, jsval *arglist)
 			return JS_FALSE;
 	}
 	rc = JS_SUSPENDREQUEST(cx);
-	sbbs->cursor_left(val);
+	sbbs->term->cursor_left(val);
 	JS_RESUMEREQUEST(cx, rc);
 	return JS_TRUE;
 }
@@ -2163,7 +2165,7 @@ js_backspace(JSContext *cx, uintN argc, jsval *arglist)
 			return JS_FALSE;
 	}
 	rc = JS_SUSPENDREQUEST(cx);
-	sbbs->backspace(val);
+	sbbs->term->backspace(val);
 	JS_RESUMEREQUEST(cx, rc);
 	return JS_TRUE;
 }
@@ -2171,21 +2173,15 @@ js_backspace(JSContext *cx, uintN argc, jsval *arglist)
 static JSBool
 js_creturn(JSContext *cx, uintN argc, jsval *arglist)
 {
-	jsval *    argv = JS_ARGV(cx, arglist);
 	sbbs_t*    sbbs;
 	jsrefcount rc;
-	int32      val = 1;
 
 	if ((sbbs = (sbbs_t*)js_GetClassPrivate(cx, JS_THIS_OBJECT(cx, arglist), &js_console_class)) == NULL)
 		return JS_FALSE;
 
 	JS_SET_RVAL(cx, arglist, JSVAL_VOID);
-	if (argc) {
-		if (!JS_ValueToInt32(cx, argv[0], &val))
-			return JS_FALSE;
-	}
 	rc = JS_SUSPENDREQUEST(cx);
-	sbbs->carriage_return(val);
+	sbbs->term->carriage_return();
 	JS_RESUMEREQUEST(cx, rc);
 	return JS_TRUE;
 }
@@ -2207,7 +2203,7 @@ js_linefeed(JSContext *cx, uintN argc, jsval *arglist)
 			return JS_FALSE;
 	}
 	rc = JS_SUSPENDREQUEST(cx);
-	sbbs->line_feed(val);
+	sbbs->term->line_feed(val);
 	JS_RESUMEREQUEST(cx, rc);
 	return JS_TRUE;
 }
@@ -2237,7 +2233,7 @@ js_ansi_getdims(JSContext *cx, uintN argc, jsval *arglist)
 		return JS_FALSE;
 
 	rc = JS_SUSPENDREQUEST(cx);
-	JS_SET_RVAL(cx, arglist, BOOLEAN_TO_JSVAL(sbbs->ansi_getdims()));
+	JS_SET_RVAL(cx, arglist, BOOLEAN_TO_JSVAL(sbbs->term->getdims()));
 	JS_RESUMEREQUEST(cx, rc);
 	return JS_TRUE;
 }
@@ -2357,11 +2353,11 @@ js_term_supports(JSContext *cx, uintN argc, jsval *arglist)
 		if (!JS_ValueToInt32(cx, argv[0], &flags))
 			return JS_FALSE;
 		rc = JS_SUSPENDREQUEST(cx);
-		JS_SET_RVAL(cx, arglist, BOOLEAN_TO_JSVAL(sbbs->term_supports(flags)));
+		JS_SET_RVAL(cx, arglist, BOOLEAN_TO_JSVAL(sbbs->term->supports(flags)));
 		JS_RESUMEREQUEST(cx, rc);
 	} else {
 		rc = JS_SUSPENDREQUEST(cx);
-		flags = sbbs->term_supports();
+		flags = sbbs->term->flags();
 		JS_RESUMEREQUEST(cx, rc);
 		JS_SET_RVAL(cx, arglist, INT_TO_JSVAL(flags));
 	}
@@ -2662,7 +2658,7 @@ static jsSyncMethodSpec js_console_functions[] = {
 	},
 	{"crlf",            js_newline,         0, JSTYPE_ALIAS },
 	{"newline",         js_newline,         0, JSTYPE_VOID,     JSDOCSTR("[count=1]")
-	 , JSDOCSTR("Output <i>count</i> number of new-line sequences (e.g. carriage-return/line-feed pairs), AKA <tt>crlf()</tt>")
+	 , JSDOCSTR("Output <i>count</i> number of new-line sequences (e.g. carriage-return/line-feed pairs), AKA <tt>crlf() does perform pause</tt>")
 	 , 310
 	},
 	{"cond_newline",    js_cond_newline,    0,  JSTYPE_VOID,    JSDOCSTR("")
@@ -2840,12 +2836,12 @@ static jsSyncMethodSpec js_console_functions[] = {
 	 , JSDOCSTR("Send a destructive backspace sequence")
 	 , 315
 	},
-	{"creturn",         js_creturn,         0, JSTYPE_VOID,     JSDOCSTR("[count=1]")
+	{"creturn",         js_creturn,         0, JSTYPE_VOID,     JSDOCSTR("")
 	 , JSDOCSTR("Send carriage-return (or equivalent) character(s) - moving the cursor to the left-most screen column")
 	 , 31700
 	},
 	{"linefeed",        js_linefeed,        0, JSTYPE_VOID,     JSDOCSTR("[count=1]")
-	 , JSDOCSTR("Send line-feed (or equivalent) character(s) - moving the cursor down one or more screen rows")
+	 , JSDOCSTR("Send line-feed (or equivalent) character(s) - moving the cursor down one or more screen rows, does not cause a pause")
 	 , 320
 	},
 	{"clearkeybuffer",  js_clearkeybuf,     0, JSTYPE_VOID,     JSDOCSTR("")
diff --git a/src/sbbs3/js_user.c b/src/sbbs3/js_user.c
index e9bff784967a5e581fe4b141027244ace12863e0..b08ec185af8bc7313654de9d9a816177c20ebcd1 100644
--- a/src/sbbs3/js_user.c
+++ b/src/sbbs3/js_user.c
@@ -22,6 +22,7 @@
 #include "sbbs.h"
 #include "filedat.h"
 #include "js_request.h"
+#include "terminal.h"
 
 #ifdef JAVASCRIPT
 
@@ -468,6 +469,7 @@ static JSBool js_user_set(JSContext *cx, JSObject *obj, jsid id, JSBool strict,
 	int32      usernumber;
 	jsrefcount rc;
 	scfg_t*    scfg;
+	void*      ptr;
 
 	scfg = JS_GetRuntimePrivate(JS_GetRuntime(cx));
 
@@ -612,8 +614,10 @@ static JSBool js_user_set(JSContext *cx, JSObject *obj, jsid id, JSBool strict,
 				free(str);
 				return JS_FALSE;
 			}
-			putusermisc(scfg, p->user->number, p->user->misc = val);
+			ptr = JS_GetContextPrivate(cx);
 			rc = JS_SUSPENDREQUEST(cx);
+			putusermisc(scfg, p->user->number, p->user->misc = val);
+			update_terminal(ptr, p->user);
 			break;
 		case USER_PROP_QWK:
 			JS_RESUMEREQUEST(cx, rc);
diff --git a/src/sbbs3/listfile.cpp b/src/sbbs3/listfile.cpp
index 3be3c3b341fcb768e4afc4c2eded9d4401e9e981..a0eb7f2746efa0fc4a6dbea2efa809018cb2c94d 100644
--- a/src/sbbs3/listfile.cpp
+++ b/src/sbbs3/listfile.cpp
@@ -110,7 +110,7 @@ int sbbs_t::listfiles(const int dirnum, const char *filespec, FILE* tofile, cons
 			if (useron.misc & BATCHFLAG && !tofile && found && found != lastbat
 			    && !(mode & (FL_EXT | FL_VIEW))) {
 				flagprompt = 0;
-				lncntr = 0;
+				term->lncntr = 0;
 				if ((i = batchflagprompt(&smb, bf, file_row, letter - 'A', file_count)) == 2) {
 					m = anchor;
 					found -= letter - 'A';
@@ -202,19 +202,19 @@ int sbbs_t::listfiles(const int dirnum, const char *filespec, FILE* tofile, cons
 						bputs("\xbb\r\n\xba ");
 						snprintf(hdr, sizeof hdr, text[BoxHdrLib], i + 1, cfg.lib[usrlib[i]]->lname);
 						bputs(hdr);
-						for (c = bstrlen(hdr); c < d; c++)
+						for (c = term->bstrlen(hdr); c < d; c++)
 							outchar(' ');
 						attr(cfg.color[clr_filelsthdrbox]);
 						bputs("\xba\r\n\xba ");
 						snprintf(hdr, sizeof hdr, text[BoxHdrDir], j + 1, cfg.dir[dirnum]->lname);
 						bputs(hdr);
-						for (c = bstrlen(hdr); c < d; c++)
+						for (c = term->bstrlen(hdr); c < d; c++)
 							outchar(' ');
 						attr(cfg.color[clr_filelsthdrbox]);
 						bputs("\xba\r\n\xba ");
 						snprintf(hdr, sizeof hdr, text[BoxHdrFiles], file_count);
 						bputs(hdr);
-						for (c = bstrlen(hdr); c < d; c++)
+						for (c = term->bstrlen(hdr); c < d; c++)
 							outchar(' ');
 						attr(cfg.color[clr_filelsthdrbox]);
 						bputs("\xba\r\n\xc8\xcd");
@@ -232,7 +232,7 @@ int sbbs_t::listfiles(const int dirnum, const char *filespec, FILE* tofile, cons
 					snprintf(hdr, sizeof hdr, text[ShortHdrLib], i + 1, cfg.lib[usrlib[i]]->sname);
 					bputs("\1[\1>\r\n");
 					bputs(hdr);
-					c = bstrlen(hdr);
+					c = term->bstrlen(hdr);
 				}
 				if (tofile) {
 					c += fprintf(tofile, "(%u) %s", j + 1, cfg.dir[dirnum]->lname);
@@ -240,7 +240,7 @@ int sbbs_t::listfiles(const int dirnum, const char *filespec, FILE* tofile, cons
 				else {
 					snprintf(hdr, sizeof hdr, text[ShortHdrDir], j + 1, cfg.dir[dirnum]->lname);
 					bputs(hdr);
-					c += bstrlen(hdr);
+					c += term->bstrlen(hdr);
 				}
 				if (tofile) {
 					fprintf(tofile, "\r\n%.*s\r\n", c, "----------------------------------------------------------------");
@@ -254,7 +254,7 @@ int sbbs_t::listfiles(const int dirnum, const char *filespec, FILE* tofile, cons
 				}
 			}
 		}
-		int currow = row;
+		int currow = term->row;
 		next = m;
 		disp = 1;
 		if (mode & (FL_EXT | FL_VIEW)) {
@@ -295,10 +295,10 @@ int sbbs_t::listfiles(const int dirnum, const char *filespec, FILE* tofile, cons
 			if (flagprompt || letter == 'Z' || !disp ||
 			    (filespec[0] && !strchr(filespec, '*') && !strchr(filespec, '?')
 			     && !(mode & FL_FIND))
-			    || (useron.misc & BATCHFLAG && !tofile && lncntr >= rows - 2)
+			    || (useron.misc & BATCHFLAG && !tofile && term->lncntr >= term->rows - 2)
 			    ) {
 				flagprompt = 0;
-				lncntr = 0;
+				term->lncntr = 0;
 				lastbat = found;
 				if ((int)(i = batchflagprompt(&smb, bf, file_row, letter - 'A' + 1, file_count)) < 1) {
 					if ((int)i == -1)
@@ -327,8 +327,8 @@ int sbbs_t::listfiles(const int dirnum, const char *filespec, FILE* tofile, cons
 				letter++;
 		}
 		if (useron.misc & BATCHFLAG && !tofile
-		    && lncntr >= rows - 2) {
-			lncntr = 0;       /* defeat pause() */
+		    && term->lncntr >= term->rows - 2) {
+			term->lncntr = 0;       /* defeat pause() */
 			flagprompt = 1;
 		}
 		m = next;
@@ -365,16 +365,16 @@ bool sbbs_t::listfile(file_t* f, const int dirnum, const char *search, const cha
 			FREE_AND_NULL(ext);
 			if (ch != '\0') {
 				ext = f->extdesc;
-				if ((useron.misc & BATCHFLAG) && lncntr + extdesclines(ext) >= rows - 2 && letter != 'A')
+				if ((useron.misc & BATCHFLAG) && term->lncntr + extdesclines(ext) >= term->rows - 2 && letter != 'A')
 					return false;
 			}
 		}
 	}
 
-	cond_newline();
+	term->cond_newline();
 	attr(cfg.color[(f->hdr.attr & MSG_DELETE) ? clr_err : clr_filename]);
 	char fname[SMB_FILEIDX_NAMELEN + 1];
-	if (namelen < 12 || cols < 132)
+	if (namelen < 12 || term->cols < 132)
 		namelen = 12;
 	else if (namelen > sizeof(fname) - 1)
 		namelen = sizeof(fname) - 1;
@@ -459,6 +459,22 @@ bool sbbs_t::listfile(file_t* f, const int dirnum, const char *search, const cha
 	return true;
 }
 
+static int
+mouse_list(sbbs_t *sbbs, uint *row, const int total, char *str)
+{
+	int d;
+	link_list_t tmp_hotspots {};
+	link_list_t *saved_hotspots = sbbs->term->mouse_hotspots;
+	sbbs->term->mouse_hotspots = &tmp_hotspots;
+	for (int i = 0; i < total; i++)
+		sbbs->term->add_hotspot((char)('A' + i), /* hungry: */ true, HOTSPOT_CURRENT_X, HOTSPOT_CURRENT_X, row[i]);
+	sbbs->bputs(sbbs->text[BatchDlFlags]);
+	d = sbbs->getstr(str, BF_MAX, K_NOCRLF);
+	sbbs->term->clear_hotspots();
+	sbbs->term->mouse_hotspots = saved_hotspots;
+	return d;
+}
+
 /****************************************************************************/
 /* Batch flagging prompt for download, extended info, and archive viewing	*/
 /* Returns -1 if 'Q' or Ctrl-C, 0 if skip, 1 if [Enter], 2 otherwise        */
@@ -483,7 +499,7 @@ int sbbs_t::batchflagprompt(smb_t* smb, file_t** bf, uint* row, const int total
 		if (usrdir[ulib][udir] == smb->dirnum)
 			break;
 
-	cond_blankline();
+	term->cond_blankline();
 	while (online) {
 		bprintf(text[BatchFlagPrompt]
 		        , ulib + 1
@@ -492,10 +508,10 @@ int sbbs_t::batchflagprompt(smb_t* smb, file_t** bf, uint* row, const int total
 		        , cfg.dir[smb->dirnum]->sname
 		        , total, totalfiles);
 		ch = getkey(K_UPPER);
-		clearline();
+		term->clearline();
 		if (ch == '?') {
 			menu("batflag");
-			if (lncntr)
+			if (term->lncntr)
 				pause();
 			return 2;
 		}
@@ -522,21 +538,14 @@ int sbbs_t::batchflagprompt(smb_t* smb, file_t** bf, uint* row, const int total
 				CRLF;
 				return 2;
 			}
-			link_list_t saved_hotspots = mouse_hotspots;
-			ZERO_VAR(mouse_hotspots);
-			for (i = 0; i < total; i++)
-				add_hotspot((char)('A' + i), /* hungry: */ true, -1, -1, row[i]);
-			bputs(text[BatchDlFlags]);
-			d = getstr(str, BF_MAX, K_NOCRLF);
-			clear_hotspots();
-			mouse_hotspots = saved_hotspots;
-			lncntr = 0;
+			d = mouse_list(this, row, total, str);
+			term->lncntr = 0;
 			if (sys_status & SS_ABORT)
 				return -1;
 			if (d > 0) {     /* d is string length */
 				strupr(str);
 				CRLF;
-				lncntr = 0;
+				term->lncntr = 0;
 				for (c = 0; c < d; c++) {
 					if (batdn_total() >= cfg.max_batdn) {
 						bprintf(text[BatchDlQueueIsFull], str + c);
@@ -570,7 +579,7 @@ int sbbs_t::batchflagprompt(smb_t* smb, file_t** bf, uint* row, const int total
 				CRLF;
 				return 2;
 			}
-			clearline();
+			term->clearline();
 			continue;
 		}
 
@@ -580,21 +589,14 @@ int sbbs_t::batchflagprompt(smb_t* smb, file_t** bf, uint* row, const int total
 					return -1;
 				return 2;
 			}
-			link_list_t saved_hotspots = mouse_hotspots;
-			ZERO_VAR(mouse_hotspots);
-			for (i = 0; i < total; i++)
-				add_hotspot((char)('A' + i), /* hungry: */ true, -1, -1, row[i]);
-			bputs(text[BatchDlFlags]);
-			d = getstr(str, BF_MAX, K_NOCRLF);
-			clear_hotspots();
-			mouse_hotspots = saved_hotspots;
-			lncntr = 0;
+			d = mouse_list(this, row, total, str);
+			term->lncntr = 0;
 			if (sys_status & SS_ABORT)
 				return -1;
 			if (d > 0) {     /* d is string length */
 				strupr(str);
 				CRLF;
-				lncntr = 0;
+				term->lncntr = 0;
 				for (c = 0; c < d; c++) {
 					if (str[c] == '*' || strchr(str + c, '.')) {     /* filename or spec given */
 //						f.dir=dirnum;
@@ -617,10 +619,10 @@ int sbbs_t::batchflagprompt(smb_t* smb, file_t** bf, uint* row, const int total
 							return -1;
 					}
 				}
-				cond_newline();
+				term->cond_newline();
 				return 2;
 			}
-			clearline();
+			term->clearline();
 			continue;
 		}
 
@@ -632,22 +634,15 @@ int sbbs_t::batchflagprompt(smb_t* smb, file_t** bf, uint* row, const int total
 				d = 1;
 			}
 			else {
-				link_list_t saved_hotspots = mouse_hotspots;
-				ZERO_VAR(mouse_hotspots);
-				for (i = 0; i < total; i++)
-					add_hotspot((char)('A' + i), /* hungry: */ true, -1, -1, row[i]);
-				bputs(text[BatchDlFlags]);
-				d = getstr(str, BF_MAX, K_NOCRLF);
-				clear_hotspots();
-				mouse_hotspots = saved_hotspots;
+				d = mouse_list(this, row, total, str);
 			}
-			lncntr = 0;
+			term->lncntr = 0;
 			if (sys_status & SS_ABORT)
 				return -1;
 			if (d > 0) {     /* d is string length */
 				strupr(str);
 				if (total > 1)
-					newline();
+					term->newline();
 				if (ch == 'R') {
 					if (noyes(text[RemoveFileQ]))
 						return 2;
@@ -684,7 +679,7 @@ int sbbs_t::batchflagprompt(smb_t* smb, file_t** bf, uint* row, const int total
 						md--;
 					CRLF;
 				}
-				lncntr = 0;
+				term->lncntr = 0;
 				for (c = 0; c < d; c++) {
 					if (str[c] == '*' || strchr(str + c, '.')) {     /* filename or spec given */
 //						f.dir=dirnum;
@@ -732,7 +727,7 @@ int sbbs_t::batchflagprompt(smb_t* smb, file_t** bf, uint* row, const int total
 				}
 				return 2;
 			}
-			clearline();
+			term->clearline();
 			continue;
 		}
 
@@ -839,7 +834,7 @@ int sbbs_t::listfileinfo(const int dirnum, const char *filespec, const int mode)
 		}
 		else {
 			showfileinfo(f, /* show_extdesc: */ mode != FI_DOWNLOAD);
-//			newline();
+//			term->newline();
 		}
 		if (mode == FI_REMOVE || mode == FI_OLD || mode == FI_OLDUL
 		    || mode == FI_OFFLINE) {
@@ -1057,7 +1052,7 @@ int sbbs_t::listfileinfo(const int dirnum, const char *filespec, const int mode)
 				if (i < cfg.total_prots) {
 					delfiles(cfg.temp_dir, ALLFILES);
 					if (cfg.dir[f->dir]->seqdev) {
-						lncntr = 0;
+						term->lncntr = 0;
 						seqwait(cfg.dir[f->dir]->seqdev);
 						bprintf(text[RetrievingFile], f->name);
 						getfilepath(&cfg, f, str);
@@ -1075,7 +1070,7 @@ int sbbs_t::listfileinfo(const int dirnum, const char *filespec, const int mode)
 							bputs(cfg.dlevent[j]->workstr);
 							external(cmdstr(cfg.dlevent[j]->cmd, path, nulstr, NULL, cfg.dlevent[j]->ex_mode)
 							         , cfg.dlevent[j]->ex_mode);
-							clearline();
+							term->clearline();
 						}
 					}
 					putnode_downloading(getfilesize(&cfg, f));
diff --git a/src/sbbs3/logon.cpp b/src/sbbs3/logon.cpp
index 8a361e963c11abcbda01f9667b2a9f9b45c79bef..e8670a74d5a133de49e279fa78c8eea432a63906 100644
--- a/src/sbbs3/logon.cpp
+++ b/src/sbbs3/logon.cpp
@@ -215,9 +215,9 @@ bool sbbs_t::logon()
 
 	bputs(text[LoggingOn]);
 	if (useron.rows != TERM_ROWS_AUTO)
-		rows = useron.rows;
+		term->rows = useron.rows;
 	if (useron.cols != TERM_COLS_AUTO)
-		cols = useron.cols;
+		term->cols = useron.cols;
 	update_nodeterm();
 	if (tm.tm_mon + 1 == getbirthmonth(&cfg, useron.birth) && tm.tm_mday == getbirthday(&cfg, useron.birth)
 	    && !(useron.rest & FLAG('Q'))) {
@@ -456,6 +456,8 @@ bool sbbs_t::logon()
 	putuserdat(&useron);
 	getmsgptrs();
 	sys_status |= SS_USERON;          /* moved from further down */
+	// Needs to be called after SS_USERON is set
+	update_nodeterm();
 
 	mqtt_user_login(mqtt, &client);
 
@@ -520,7 +522,7 @@ bool sbbs_t::logon()
 		bprintf(text[LiMailWaiting], mailw, mailw - mailr);
 		bprintf(text[LiSysopIs]
 		        , text[sysop_available(&cfg) ? LiSysopAvailable : LiSysopNotAvailable]);
-		newline();
+		term->newline();
 	}
 
 	if (sys_status & SS_EVENT)
diff --git a/src/sbbs3/main.cpp b/src/sbbs3/main.cpp
index 9d80a56e3319f1e24e1b75cf73fa188052d7e5a2..e5c8e95d04d52f1d34bbdae9b55a289fddefddbb 100644
--- a/src/sbbs3/main.cpp
+++ b/src/sbbs3/main.cpp
@@ -1086,7 +1086,7 @@ js_write_raw(JSContext *cx, uintN argc, jsval *arglist)
 		if (len < 1)
 			continue;
 		rc = JS_SUSPENDREQUEST(cx);
-		sbbs->putcom(str, len);
+		sbbs->term_out(str, len);
 		JS_RESUMEREQUEST(cx, rc);
 	}
 	if (str != NULL)
@@ -2551,8 +2551,6 @@ void output_thread(void* arg)
 	lprintf(LOG_DEBUG, "%s output thread started", node);
 #endif
 
-	sbbs->console |= CON_R_ECHO;
-
 #ifdef TCP_MAXSEG
 	/*
 	 * Auto-tune the highwater mark to be the negotiated MSS for the
@@ -2982,12 +2980,10 @@ void event_thread(void* arg)
 						continue;
 					}
 					sbbs->online = ON_LOCAL;
-					sbbs->console |= CON_L_ECHO;
 					sbbs->getusrsubs();
 					bool success = sbbs->unpack_rep(fname);
 					sbbs->delfiles(sbbs->cfg.temp_dir, ALLFILES);        /* clean-up temp_dir after unpacking */
 					sbbs->online = false;
-					sbbs->console &= ~CON_L_ECHO;
 
 					/* putuserdat? */
 					if (success) {
@@ -3050,7 +3046,6 @@ void event_thread(void* arg)
 				if (!(sbbs->useron.misc & (DELETED | INACTIVE))) {
 					sbbs->lprintf(LOG_INFO, "Packing QWK Message Packet");
 					sbbs->online = ON_LOCAL;
-					sbbs->console |= CON_L_ECHO;
 					sbbs->getmsgptrs();
 					sbbs->getusrsubs();
 
@@ -3066,7 +3061,6 @@ void event_thread(void* arg)
 					} else
 						sbbs->lputs(LOG_INFO, "No packet created (no new messages)");
 					sbbs->delfiles(sbbs->cfg.temp_dir, ALLFILES);
-					sbbs->console &= ~CON_L_ECHO;
 					sbbs->online = false;
 				}
 				sbbs->fremove(WHERE, fname);
@@ -3105,12 +3099,10 @@ void event_thread(void* arg)
 
 						sbbs->lprintf(LOG_INFO, "Running node %d daily event", i);
 						sbbs->online = ON_LOCAL;
-						sbbs->console |= CON_L_ECHO;
 						sbbs->logentry("!:", "Run node daily event");
 						const char* cmd = sbbs->cmdstr(sbbs->cfg.node_daily.cmd, nulstr, nulstr, NULL, sbbs->cfg.node_daily.misc);
 						int result = sbbs->external(cmd, EX_OFFLINE | sbbs->cfg.node_daily.misc);
 						sbbs->lprintf(result ? LOG_ERR : LOG_INFO, "Node daily event: '%s' returned %d", cmd, result);
-						sbbs->console &= ~CON_L_ECHO;
 						sbbs->online = false;
 					}
 					if (sbbs->getnodedat(i, &node, true)) {
@@ -3177,7 +3169,6 @@ void event_thread(void* arg)
 					if (flength(str) > 0) {    /* silently ignore 0-byte QWK packets */
 						sbbs->lprintf(LOG_DEBUG, "Inbound QWK Packet detected: %s", str);
 						sbbs->online = ON_LOCAL;
-						sbbs->console |= CON_L_ECHO;
 						if (sbbs->unpack_qwk(str, i) == false) {
 							char newname[MAX_PATH + 1];
 							SAFEPRINTF2(newname, "%s.%x.bad", str, (int)now);
@@ -3191,7 +3182,6 @@ void event_thread(void* arg)
 							sbbs->delfiles(sbbs->cfg.data_dir, newname, /* keep: */ 10);
 						}
 						sbbs->delfiles(sbbs->cfg.temp_dir, ALLFILES);
-						sbbs->console &= ~CON_L_ECHO;
 						sbbs->online = false;
 						if (fexist(str))
 							sbbs->fremove(WHERE, str, /* log-all-errors: */ true);
@@ -3220,9 +3210,7 @@ void event_thread(void* arg)
 				}
 				if (file != -1)
 					close(file);
-				sbbs->console |= CON_L_ECHO;
 				packed_rep = sbbs->pack_rep(i);
-				sbbs->console &= ~CON_L_ECHO;
 				if (packed_rep) {
 					if ((file = sbbs->nopen(str, O_WRONLY | O_CREAT)) == -1)
 						sbbs->errormsg(WHERE, ERR_OPEN, str, O_WRONLY | O_CREAT);
@@ -3270,14 +3258,12 @@ void event_thread(void* arg)
 					SAFECOPY(sbbs->cfg.node_dir, sbbs->cfg.node_path[sbbs->cfg.node_num - 1]);
 					sbbs->lprintf(LOG_INFO, "Call-out: %s", sbbs->cfg.qhub[i]->id);
 					sbbs->online = ON_LOCAL;
-					sbbs->console |= CON_L_ECHO;
 					int ex_mode = EX_OFFLINE | EX_SH; /* sh for Unix perl scripts */
 					if (sbbs->cfg.qhub[i]->misc & QHUB_NATIVE)
 						ex_mode |= EX_NATIVE;
 					const char* cmd = sbbs->cmdstr(sbbs->cfg.qhub[i]->call, sbbs->cfg.qhub[i]->id, sbbs->cfg.qhub[i]->id, NULL, ex_mode);
 					int result = sbbs->external(cmd, ex_mode);
 					sbbs->lprintf(result ? LOG_ERR : LOG_INFO, "Call-out to: %s (%s) returned %d", sbbs->cfg.qhub[i]->id, cmd, result);
-					sbbs->console &= ~CON_L_ECHO;
 					sbbs->online = false;
 				}
 			}
@@ -3455,7 +3441,6 @@ void event_thread(void* arg)
 						ex_mode |= EX_SH;
 					ex_mode |= (sbbs->cfg.event[i]->misc & EX_NATIVE);
 					sbbs->online = ON_LOCAL;
-					sbbs->console |= CON_L_ECHO;
 					cmd = sbbs->cmdstr(cmd, nulstr, sbbs->cfg.event[i]->dir, NULL, ex_mode);
 					sbbs->lprintf(LOG_INFO, "Running %s%stimed event: %s"
 					              , native_executable(&sbbs->cfg, cmd, ex_mode) ? "native ":"16-bit DOS "
@@ -3468,7 +3453,6 @@ void event_thread(void* arg)
 						else
 							sbbs->lprintf(LOG_DEBUG, "Background timed event spawned: %s", cmd);
 					}
-					sbbs->console &= ~CON_L_ECHO;
 					sbbs->online = false;
 					sbbs->cfg.event[i]->last = time32(NULL);
 					SAFEPRINTF(str, "%stime.ini", sbbs->cfg.ctrl_dir);
@@ -3570,9 +3554,7 @@ sbbs_t::sbbs_t(ushort node_num, union xp_sockaddr *addr, size_t addr_len, const
 	SAFECOPY(connection, "Telnet");
 	telnet_ack_event = CreateEvent(NULL, /* Manual Reset: */ false, /* InitialState */ false, NULL);
 
-	listInit(&savedlines, /* flags: */ 0);
 	listInit(&smb_list, /* flags: */ 0);
-	listInit(&mouse_hotspots, /* flags: */ 0);
 	pthread_mutex_init(&nodefile_mutex, NULL);
 
 	for (i = 0; i < TOTAL_TEXT; i++)
@@ -3823,6 +3805,7 @@ bool sbbs_t::init()
 	pthread_mutex_init(&input_thread_mutex, NULL);
 	input_thread_mutex_created = true;
 
+	update_terminal(this);
 	reset_logon_vars();
 
 	online = ON_REMOTE;
@@ -3930,9 +3913,7 @@ sbbs_t::~sbbs_t()
 	FREE_AND_NULL(qwknode);
 	total_qwknodes = 0;
 
-	listFree(&savedlines);
 	listFree(&smb_list);
-	listFree(&mouse_hotspots);
 
 #ifdef USE_CRYPTLIB
 	while (ssh_mutex_created && pthread_mutex_destroy(&ssh_mutex) == EBUSY)
@@ -3952,6 +3933,11 @@ sbbs_t::~sbbs_t()
 		lprintf(LOG_ERR, "!MEMORY ERRORS REPORTED IN DATA/DEBUG.LOG!");
 #endif
 
+	if (term) {
+		delete term;
+		term = nullptr;
+	}
+
 #ifdef _DEBUG
 	lprintf(LOG_DEBUG, "destructor end");
 #endif
@@ -4077,7 +4063,7 @@ int sbbs_t::mv(const char* path, const char* dest, bool copy)
 void sbbs_t::hangup(void)
 {
 	if (online) {
-		clear_hotspots();
+		term->clear_hotspots();
 		lprintf(LOG_DEBUG, "disconnecting client");
 		online = false;   // moved from the bottom of this function on Jan-25-2009
 	}
@@ -4266,13 +4252,16 @@ void sbbs_t::reset_logon_vars(void)
 	cid[0] = 0;
 	wordwrap[0] = 0;
 	question[0] = 0;
-	row = 0;
-	rows = startup->default_term_height;
-	cols = startup->default_term_width;
-	lncntr = 0;
+	if (term) {
+		term->row = 0;
+		term->rows = startup->default_term_height;
+		term->cols = startup->default_term_width;
+		term->lncntr = 0;
+		term->cterm_version = 0;
+		term->lbuflen = 0;
+		term->cur_output_rate = output_rate_unlimited;
+	}
 	autoterm = 0;
-	cterm_version = 0;
-	lbuflen = 0;
 	timeleft_warn = 0;
 	keybufbot = keybuftop = 0;
 	usrgrps = usrlibs = 0;
@@ -4283,7 +4272,6 @@ void sbbs_t::reset_logon_vars(void)
 		cursub[i] = 0;
 	cur_rate = 30000;
 	dte_rate = 38400;
-	cur_output_rate = output_rate_unlimited;
 	main_cmds = xfer_cmds = posts_read = 0;
 	lastnodemsg = 0;
 	lastnodemsguser[0] = 0;
@@ -5608,6 +5596,13 @@ NO_SSH:
 				close_socket(client_socket);
 				continue;
 			}
+			if (inet_addrport(&local_addr) == startup->pet40_port || inet_addrport(&local_addr) == startup->pet80_port) {
+				sbbs->autoterm = PETSCII;
+				sbbs->term->cols = inet_addrport(&local_addr) == startup->pet40_port ? 40 : 80;
+				sbbs->term_out(PETSCII_UPPERLOWER);
+			}
+			update_terminal(sbbs);
+			// TODO: Plain text output in SSH socket
 			struct trash trash;
 			if (sbbs->trashcan(host_ip, "ip", &trash)) {
 				char details[128];
@@ -5680,12 +5675,7 @@ NO_SSH:
 				sbbs->outcom(0); /* acknowledge RLogin per RFC 1282 */
 
 			sbbs->autoterm = 0;
-			sbbs->cols = startup->default_term_width;
-			if (inet_addrport(&local_addr) == startup->pet40_port || inet_addrport(&local_addr) == startup->pet80_port) {
-				sbbs->autoterm = PETSCII;
-				sbbs->cols = inet_addrport(&local_addr) == startup->pet40_port ? 40 : 80;
-				sbbs->outcom(PETSCII_UPPERLOWER);
-			}
+			sbbs->term->cols = startup->default_term_width;
 
 			sbbs->bprintf("\r\n%s\r\n", VERSION_NOTICE);
 			sbbs->bprintf("%s connection from: %s\r\n", client.protocol, host_ip);
@@ -5694,7 +5684,7 @@ NO_SSH:
 			if (!(startup->options & BBS_OPT_NO_HOST_LOOKUP)) {
 				sbbs->bprintf("Resolving hostname...");
 				getnameinfo(&client_addr.addr, client_addr_len, host_name, sizeof(host_name), NULL, 0, NI_NAMEREQD);
-				sbbs->putcom(crlf);
+				sbbs->cp437_out(crlf);
 				lprintf(LOG_INFO, "%04d %s [%s] Hostname: %s", client_socket, client.protocol, host_ip, host_name);
 			}
 
@@ -5721,7 +5711,7 @@ NO_SSH:
 							lprintf(LOG_INFO, "%04d %s [%s] Identity: %s", client_socket, client.protocol, host_ip, identity);
 					}
 				}
-				sbbs->putcom(crlf);
+				sbbs->cp437_out(crlf);
 			}
 			/* Initialize client display */
 			client.size = sizeof(client);
@@ -5783,8 +5773,8 @@ NO_SSH:
 				if (fexist(str))
 					sbbs->printfile(str, P_NOABORT);
 				else {
-					sbbs->putcom("\r\nSorry, all terminal nodes are in use or otherwise unavailable.\r\n");
-					sbbs->putcom("Please try again later.\r\n");
+					sbbs->cp437_out("\r\nSorry, all terminal nodes are in use or otherwise unavailable.\r\n");
+					sbbs->cp437_out("Please try again later.\r\n");
 				}
 				sbbs->flush_output(3000);
 				client_off(client_socket);
@@ -5859,7 +5849,7 @@ NO_SSH:
 				if (fexist(str))
 					sbbs->printfile(str, P_NOABORT);
 				else
-					sbbs->putcom("\r\nSorry, initialization failed. Try again later.\r\n");
+					sbbs->cp437_out("\r\nSorry, initialization failed. Try again later.\r\n");
 				sbbs->flush_output(3000);
 				if (sbbs->getnodedat(new_node->cfg.node_num, &node, true)) {
 					node.status = NODE_WFC;
@@ -5996,10 +5986,11 @@ NO_PASSTHRU:
 
 			uint32_t client_count = protected_uint32_adjust(&node_threads_running, 1);
 			new_node->input_thread_running = true;
+			new_node->autoterm = sbbs->autoterm;
+			update_terminal(new_node, sbbs->term);
+			new_node->term->cols = sbbs->term->cols;
 			new_node->input_thread = (HANDLE)_beginthread(input_thread, 0, new_node);
 			new_node->output_thread_running = true;
-			new_node->autoterm = sbbs->autoterm;
-			new_node->cols = sbbs->cols;
 			_beginthread(output_thread, 0, new_node);
 			_beginthread(node_thread, 0, new_node);
 			served++;
diff --git a/src/sbbs3/msgtoqwk.cpp b/src/sbbs3/msgtoqwk.cpp
index 0e0c7534d82952482f39ed1152d12561e1fbbc91..85b7723da20cfc6b7bc7ce6c29891bd3bfab820b 100644
--- a/src/sbbs3/msgtoqwk.cpp
+++ b/src/sbbs3/msgtoqwk.cpp
@@ -20,6 +20,7 @@
  ****************************************************************************/
 
 #include "sbbs.h"
+#include "ansi_terminal.h"
 #include "qwk.h"
 #include "utf8.h"
 #include "cp437defs.h"
@@ -258,7 +259,7 @@ int sbbs_t::msgtoqwk(smbmsg_t* msg, FILE *qwk_fp, int mode, smb_t* smb
 	}
 	if (mode & QM_WORDWRAP) {
 		int   org_cols = msg->columns ? msg->columns : 80;
-		int   new_cols = useron.cols ? useron.cols : cols ? cols : 80;
+		int   new_cols = useron.cols ? useron.cols : term->cols ? term->cols : 80;
 		char* wrapped = ::wordwrap(buf, new_cols - 1, org_cols - 1, /* handle_quotes */ true, is_utf8, /* pipe_codes: */false);
 		if (wrapped != NULL) {
 			free(buf);
@@ -419,65 +420,66 @@ int sbbs_t::msgtoqwk(smbmsg_t* msg, FILE *qwk_fp, int mode, smb_t* smb
 					break;
 				if (mode & QM_EXPCTLA) {
 					str[0] = 0;
+					ANSI_Terminal ansi(this);
 					switch (toupper(ch)) {
 						case 'W':
-							SAFECOPY(str, ansi(LIGHTGRAY));
+							SAFECOPY(str, ansi.attrstr(LIGHTGRAY));
 							break;
 						case 'K':
-							SAFECOPY(str, ansi(BLACK));
+							SAFECOPY(str, ansi.attrstr(BLACK));
 							break;
 						case 'H':
-							SAFECOPY(str, ansi(HIGH));
+							SAFECOPY(str, ansi.attrstr(HIGH));
 							break;
 						case 'I':
-							SAFECOPY(str, ansi(BLINK));
+							SAFECOPY(str, ansi.attrstr(BLINK));
 							break;
 						case '-':
 						case '_':
 						case 'N':   /* Normal */
-							SAFECOPY(str, ansi(ANSI_NORMAL));
+							SAFECOPY(str, ansi.attrstr(ANSI_NORMAL));
 							break;
 						case 'R':
-							SAFECOPY(str, ansi(RED));
+							SAFECOPY(str, ansi.attrstr(RED));
 							break;
 						case 'G':
-							SAFECOPY(str, ansi(GREEN));
+							SAFECOPY(str, ansi.attrstr(GREEN));
 							break;
 						case 'B':
-							SAFECOPY(str, ansi(BLUE));
+							SAFECOPY(str, ansi.attrstr(BLUE));
 							break;
 						case 'C':
-							SAFECOPY(str, ansi(CYAN));
+							SAFECOPY(str, ansi.attrstr(CYAN));
 							break;
 						case 'M':
-							SAFECOPY(str, ansi(MAGENTA));
+							SAFECOPY(str, ansi.attrstr(MAGENTA));
 							break;
 						case 'Y':   /* Yellow */
-							SAFECOPY(str, ansi(BROWN));
+							SAFECOPY(str, ansi.attrstr(BROWN));
 							break;
 						case '0':
-							SAFECOPY(str, ansi(BG_BLACK));
+							SAFECOPY(str, ansi.attrstr(BG_BLACK));
 							break;
 						case '1':
-							SAFECOPY(str, ansi(BG_RED));
+							SAFECOPY(str, ansi.attrstr(BG_RED));
 							break;
 						case '2':
-							SAFECOPY(str, ansi(BG_GREEN));
+							SAFECOPY(str, ansi.attrstr(BG_GREEN));
 							break;
 						case '3':
-							SAFECOPY(str, ansi(BG_BROWN));
+							SAFECOPY(str, ansi.attrstr(BG_BROWN));
 							break;
 						case '4':
-							SAFECOPY(str, ansi(BG_BLUE));
+							SAFECOPY(str, ansi.attrstr(BG_BLUE));
 							break;
 						case '5':
-							SAFECOPY(str, ansi(BG_MAGENTA));
+							SAFECOPY(str, ansi.attrstr(BG_MAGENTA));
 							break;
 						case '6':
-							SAFECOPY(str, ansi(BG_CYAN));
+							SAFECOPY(str, ansi.attrstr(BG_CYAN));
 							break;
 						case '7':
-							SAFECOPY(str, ansi(BG_LIGHTGRAY));
+							SAFECOPY(str, ansi.attrstr(BG_LIGHTGRAY));
 							break;
 					}
 					if (str[0])
diff --git a/src/sbbs3/newuser.cpp b/src/sbbs3/newuser.cpp
index 7775414cbce12091cdd0ec6a671a3907227456c2..9fc56bfd379768413c2fbb9a99975b636b02c7ce 100644
--- a/src/sbbs3/newuser.cpp
+++ b/src/sbbs3/newuser.cpp
@@ -151,7 +151,7 @@ bool sbbs_t::newuser()
 
 		if (useron.misc & PETSCII) {
 			autoterm |= PETSCII;
-			outcom(PETSCII_UPPERLOWER);
+			term_out(PETSCII_UPPERLOWER);
 			bputs(text[PetTerminalDetected]);
 		} else {
 			if (!yesno(text[ExAsciiTerminalQ]))
diff --git a/src/sbbs3/objects.mk b/src/sbbs3/objects.mk
index d31a5fda0704a65583766ebffd9e9b857aa28b58..b08cd3be726eb891ec0a2b07ccbd9e3835141b8f 100644
--- a/src/sbbs3/objects.mk
+++ b/src/sbbs3/objects.mk
@@ -3,7 +3,8 @@
 # [MT]OBJODIR and OFILE must be pre-defined
 
 OBJS	=		$(LOAD_CFG_OBJS) \
-			$(MTOBJODIR)/ansiterm$(OFILE) \
+			$(MTOBJODIR)/ansi_parser$(OFILE) \
+			$(MTOBJODIR)/ansi_terminal$(OFILE) \
 			$(MTOBJODIR)/answer$(OFILE)\
 			$(MTOBJODIR)/atcodes$(OFILE)\
 			$(MTOBJODIR)/bat_xfer$(OFILE)\
@@ -77,6 +78,7 @@ OBJS	=		$(LOAD_CFG_OBJS) \
 			$(MTOBJODIR)/newuser$(OFILE)\
 			$(MTOBJODIR)/pack_qwk$(OFILE)\
 			$(MTOBJODIR)/pack_rep$(OFILE)\
+			$(MTOBJODIR)/petscii_term$(OFILE)\
 			$(MTOBJODIR)/postmsg$(OFILE)\
 			$(MTOBJODIR)/prntfile$(OFILE)\
 			$(MTOBJODIR)/putmsg$(OFILE)\
@@ -95,6 +97,7 @@ OBJS	=		$(LOAD_CFG_OBJS) \
 			$(MTOBJODIR)/str$(OFILE)\
 			$(MTOBJODIR)/telgate$(OFILE)\
 			$(MTOBJODIR)/telnet$(OFILE)\
+			$(MTOBJODIR)/terminal$(OFILE)\
 			$(MTOBJODIR)/text_sec$(OFILE)\
 			$(MTOBJODIR)/tmp_xfer$(OFILE)\
 			$(MTOBJODIR)/trash$(OFILE)\
diff --git a/src/sbbs3/pack_qwk.cpp b/src/sbbs3/pack_qwk.cpp
index 108375f545875e69c2308c1e4c0893a8c2897229..d2c9d987fc69a7fe91d5da06b3eeee42ffe70380 100644
--- a/src/sbbs3/pack_qwk.cpp
+++ b/src/sbbs3/pack_qwk.cpp
@@ -435,7 +435,7 @@ bool sbbs_t::pack_qwk(char *packet, uint *msgcnt, bool prepack)
 			        && cfg.sub[usrsub[i][j]]->misc & SUB_FORCED)) {
 				if (!chk_ar(cfg.sub[usrsub[i][j]]->read_ar, &useron, &client))
 					continue;
-				lncntr = 0;                       /* defeat pause */
+				term->lncntr = 0;                       /* defeat pause */
 				if (useron.rest & FLAG('Q') && !(cfg.sub[usrsub[i][j]]->misc & SUB_QNET))
 					continue;   /* QWK Net Node and not QWK networked, so skip */
 
@@ -641,7 +641,7 @@ bool sbbs_t::pack_qwk(char *packet, uint *msgcnt, bool prepack)
 			if (isdir(str))
 				continue;
 			SAFEPRINTF2(path, "%s%s", cfg.temp_dir, dirent->d_name);
-			lncntr = 0;   /* Defeat pause */
+			term->lncntr = 0;   /* Defeat pause */
 			lprintf(LOG_INFO, "Including %s in packet", str);
 			bprintf(text[RetrievingFile], str);
 			if (!mv(str, path, /* copy: */ TRUE))
@@ -670,7 +670,7 @@ bool sbbs_t::pack_qwk(char *packet, uint *msgcnt, bool prepack)
 				}
 				totalcdt += f.cost;
 			}
-			lncntr = 0;
+			term->lncntr = 0;
 			SAFEPRINTF2(tmp, "%s%s", cfg.temp_dir, filename);
 			if (!fexistcase(tmp)) {
 				seqwait(cfg.dir[f.dir]->seqdev);
diff --git a/src/sbbs3/pack_rep.cpp b/src/sbbs3/pack_rep.cpp
index c393d5e99ff1160451387a7f065a9189fa266483..e1289ca88904a23d6a506220440957c7f7726b4d 100644
--- a/src/sbbs3/pack_rep.cpp
+++ b/src/sbbs3/pack_rep.cpp
@@ -174,7 +174,7 @@ bool sbbs_t::pack_rep(uint hubnum)
 	for (i = 0; i < cfg.qhub[hubnum]->subs; i++) {
 		j = cfg.qhub[hubnum]->sub[i]->subnum;             /* j now equals the real sub num */
 		msgs = getlastmsg(j, &last, 0);
-		lncntr = 0;                       /* defeat pause */
+		term->lncntr = 0;                       /* defeat pause */
 		if (!msgs || last <= subscan[j].ptr) {
 			if (subscan[j].ptr > last) {
 				subscan[j].ptr = last;
diff --git a/src/sbbs3/petscii_term.cpp b/src/sbbs3/petscii_term.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..786de3897f2d0fd7ecc97bfbccbd557ac77fbf88
--- /dev/null
+++ b/src/sbbs3/petscii_term.cpp
@@ -0,0 +1,550 @@
+#include "petscii_term.h"
+#include "petdefs.h"
+
+// Initial work is only C64 and C128 only, not C65, X16, C116, C16, Plus/4, or VIC-20, and certainly not a PET
+
+const char *PETSCII_Terminal::attrstr(unsigned atr)
+{
+	switch (atr) {
+
+		/* Special case */
+		case ANSI_NORMAL:
+			if (subset == PETSCII_C128_80)
+				return "\x0E\x92\x8F\x9b";	// Upper/Lower, Reverse Off, Flash Off (C128), Light Gray
+			return "\x0E\x92\x9b";	// Upper/Lower, Reverse Off, Light Gray
+		case BLINK:
+		case BG_BRIGHT:
+			if (subset == PETSCII_C128_80)
+				return "\x0F";			// Flash (C128 only)
+			return "";
+
+		/* Foreground */
+		case HIGH:
+			return "\x05";			// Literally "White"
+		case BLACK:
+			return "\x90";
+		case RED:
+			return "\x1C";
+		case GREEN:
+			return "\x1E";
+		case BROWN:
+			return "\x95";
+		case BLUE:
+			return "\x1F";
+		case MAGENTA:
+			return "\x9C";
+		case CYAN:
+			return "\x9F";
+		case LIGHTGRAY:
+			return "\x9B";
+
+		/* Background */
+		case BG_BLACK:
+			return "\x92";			// Reverse off
+		case BG_RED:
+			return "\x1C\x12";
+		case BG_GREEN:
+			return "\x1E\x12";
+		case BG_BROWN:
+			return "\x95\x12";
+		case BG_BLUE:
+			return "\x1F\x12";
+		case BG_MAGENTA:
+			return "\x9C\x12";
+		case BG_CYAN:
+			return "\x9F\x12";
+		case BG_LIGHTGRAY:
+			return "\x9B\x12";
+	}
+
+	return "-Invalid use of ansi()-";
+}
+
+static unsigned
+unreverse_attr(unsigned atr)
+{
+	// This drops all background bits and REVERSED
+	return (atr & BLINK)			// Blink unchanged
+	    | ((atr & BG_BRIGHT) ? HIGH : 0)	// Put BG_BRIGHT bit into HIGH
+	    | ((atr & 0x70) >> 4);		// Background colour to Foreground
+}
+
+static unsigned
+reverse_attr(unsigned atr)
+{
+	// This drops all foreground bits and sets REVERSED
+	return REVERSED
+	    | ((atr & HIGH) ? BG_BRIGHT : 0)	// Put HIGH bit into BG_BRIGHT
+	    | (atr & BLINK)			// Blink unchanged
+	    | ((atr & 0x07) << 4);		// Foreground colour to Background
+}
+
+/*
+ * This deals with the reverse "stuff"
+ * Basically, if the background is not black, the background colour is
+ * set as the foreground colour, and REVERSED is set
+ */
+unsigned
+PETSCII_Terminal::xlat_atr(unsigned atr)
+{
+	if (atr == ANSI_NORMAL) {
+		// But convert to "normal" atr
+		atr = LIGHTGRAY;
+	}
+	if (atr == BG_BLACK) {
+		// But convert to "normal" atr
+		atr &= ~(BG_BLACK | 0x70 | BG_BRIGHT);
+	}
+	// If this is already reversed, clear background
+	if (atr & REVERSED) {
+		atr &= ~0x70;
+	}
+	else {
+		// If there is a background colour, translate to reversed with black
+		if (atr & (0x70 | BG_BRIGHT)) {
+			atr = REVERSED | unreverse_attr(atr);
+		}
+	}
+	if (subset == PETSCII_C64)
+		atr &= ~(BLINK | UNDERLINE);
+	return atr;
+}
+
+char* PETSCII_Terminal::attrstr(unsigned atr, unsigned curatr, char* str, size_t strsz)
+{
+	unsigned newatr = xlat_atr(atr);
+	unsigned oldatr = xlat_atr(curatr);
+
+	size_t sp = 0;
+
+	if (strsz < 2) {
+		if (strsz > 0)
+			str[0] = 0;
+		return str;
+	}
+
+	if (newatr & REVERSED) {
+		if (!(oldatr & REVERSED)) {
+			str[sp++] = PETSCII_REVERSE_ON;
+			oldatr = reverse_attr(oldatr);
+		}
+	}
+	else {
+		if (oldatr & REVERSED) {
+			str[sp++] = '\x92';
+			oldatr = unreverse_attr(oldatr);
+		}
+	}
+	if (sp >= (strsz - 1)) {
+		str[sp] = 0;
+		return str;
+	}
+	if (newatr & BLINK) {
+		if (!(oldatr & BLINK))
+			str[sp++] = '\x0F';	// C128
+	}
+	else {
+		if (oldatr & BLINK)
+			str[sp++] = '\x8F';	// C128
+	}
+	if (newatr & UNDERLINE) {
+		if (!(oldatr & UNDERLINE))
+			str[sp++] = '\x02';	// C128
+	}
+	else {
+		if (oldatr & UNDERLINE)
+			str[sp++] = '\x82';	// C128
+	}
+	if (sp >= (strsz - 1)) {
+		str[sp] = 0;
+		return str;
+	}
+	if ((newatr & 0x0f) != (oldatr & 0x0f)) {
+		switch (newatr & 0x0f) {
+			case BLACK:
+				str[sp++] = '\x90';
+				break;
+			case WHITE:
+				str[sp++] = PETSCII_WHITE;
+				break;
+			case DARKGRAY:
+				if (subset == PETSCII_C128_80)
+					str[sp++] = '\x98';
+				else
+					str[sp++] = '\x97';
+				break;
+			case LIGHTGRAY:
+				str[sp++] = '\x9b';
+				break;
+			case BLUE:
+				str[sp++] = PETSCII_BLUE;
+				break;
+			case LIGHTBLUE:
+				str[sp++] = '\x9a';
+				break;
+			case CYAN:
+				if (subset == PETSCII_C128_80)
+					str[sp++] = '\x97';
+				else
+					str[sp++] = '\x98';
+				break;
+			case LIGHTCYAN:
+				str[sp++] = '\x9f';
+				break;
+			case YELLOW:
+				str[sp++] = '\x9e';
+				break;
+			case BROWN:
+				str[sp++] = '\x95';
+				break;
+			case RED:
+				str[sp++] = PETSCII_RED;
+				break;
+			case LIGHTRED:
+				str[sp++] = '\x96';
+				break;
+			case GREEN:
+				str[sp++] = PETSCII_GREEN;
+				break;
+			case LIGHTGREEN:
+				str[sp++] = '\x99';
+				break;
+			case MAGENTA:
+				str[sp++] = '\x81';
+				break;
+			case LIGHTMAGENTA:
+				str[sp++] = '\x9c';
+				break;
+		}
+	}
+	str[sp] = 0;
+	return str;
+}
+
+bool PETSCII_Terminal::gotoxy(unsigned x, unsigned y)
+{
+	sbbs->term_out(PETSCII_HOME);
+	if (y > rows)
+		y = rows;
+	if (x > cols)
+		x = cols;
+	while (row < (y - 1) && sbbs->online)
+		sbbs->term_out(PETSCII_DOWN);
+	while (column < (x - 1) && sbbs->online)
+		sbbs->term_out(PETSCII_RIGHT);
+	lncntr = 0;
+	return true;
+}
+
+// Was ansi_save
+bool PETSCII_Terminal::save_cursor_pos() {
+	saved_x = column + 1;
+	saved_y = row + 1;
+	return true;
+}
+
+// Was ansi_restore
+bool PETSCII_Terminal::restore_cursor_pos() {
+	if (saved_x && saved_y) {
+		return gotoxy(saved_x, saved_y);
+	}
+	return false;
+}
+
+void PETSCII_Terminal::carriage_return()
+{
+	cursor_left(column);
+}
+
+void PETSCII_Terminal::line_feed(unsigned count)
+{
+	// Like cursor_down() but scrolls...
+	for (unsigned i = 0; i < count; i++)
+		sbbs->term_out(PETSCII_DOWN);
+}
+
+void PETSCII_Terminal::backspace(unsigned int count)
+{
+	for (unsigned i = 0; i < count; i++)
+		sbbs->term_out(PETSCII_DELETE);
+}
+
+void PETSCII_Terminal::newline(unsigned count)
+{
+	sbbs->term_out('\r');
+	sbbs->check_pause();
+}
+
+void PETSCII_Terminal::clearscreen()
+{
+	clear_hotspots();
+	sbbs->term_out(PETSCII_CLEAR);
+	lastcrcol = 0;
+}
+
+void PETSCII_Terminal::cleartoeos()
+{
+	int x = row + 1;
+	int y = column + 1;
+
+	cleartoeol();
+	while (row < rows - 1 && sbbs->online) {
+		cursor_down();
+		clearline();
+	}
+	gotoxy(x, y);
+}
+
+void PETSCII_Terminal::cleartoeol()
+{
+	unsigned s;
+	s = column;
+	while (++s <= cols && sbbs->online)
+		sbbs->term_out(" \x14");
+}
+
+void PETSCII_Terminal::clearline()
+{
+	int c = column;
+	carriage_return();
+	cleartoeol();
+	cursor_right(c);
+}
+
+void PETSCII_Terminal::cursor_home()
+{
+	sbbs->term_out(PETSCII_HOME);
+}
+
+void PETSCII_Terminal::cursor_up(unsigned count)
+{
+	for (unsigned i = 0; i < count; i++)
+		sbbs->term_out(PETSCII_UP);
+}
+
+void PETSCII_Terminal::cursor_down(unsigned count)
+{
+	for (unsigned i = 0; i < count; i++) {
+		if (row >= (rows - 1))
+			break;
+		sbbs->term_out(PETSCII_DOWN);
+	}
+}
+
+void PETSCII_Terminal::cursor_right(unsigned count)
+{
+	for (unsigned i = 0; i < count; i++) {
+		if (column >= (cols - 1))
+			break;
+		sbbs->term_out(PETSCII_RIGHT);
+	}
+}
+
+void PETSCII_Terminal::cursor_left(unsigned count)
+{
+	for (unsigned i = 0; i < count; i++) {
+		sbbs->term_out(PETSCII_LEFT);
+		if (column == 0)
+			break;
+	}
+}
+
+const char* PETSCII_Terminal::type()
+{
+	return "PETSCII";
+}
+
+void PETSCII_Terminal::set_color(int c)
+{
+	if (curatr & REVERSED) {
+		curatr &= ~(BG_BRIGHT | 0x70);
+		curatr |= ((c & 0x07) << 4);
+		if (c & HIGH)
+			curatr |= BG_BRIGHT;
+	}
+	else {
+		curatr &= ~0x0F;
+		curatr |= c;
+	}
+}
+
+bool PETSCII_Terminal::parse_output(char ch)
+{
+	switch (ch) {
+		// Zero-width characters we likely shouldn't send
+		case 0:
+		case 1:
+		case 3:  // Stop
+		case 4:
+		case 6:
+		case 7:
+		case 9:
+		case 10:
+		case 11:
+		case 16:
+		case 21:
+		case 22:
+		case 23:
+		case 24:
+		case 25:
+		case 26:
+		case 27: // ESC - This one is especially troubling
+		case '\x80':
+		case '\x83': // Run
+		case '\x84':
+		case '\x85': // F1
+		case '\x86': // F3
+		case '\x87': // F5
+		case '\x88': // F7
+		case '\x89': // F2
+		case '\x8A': // F4
+		case '\x8B': // F6
+		case '\x8C': // F8
+			return false;
+
+		// Specials that affect cursor position
+		case '\x8D': // Shift-return
+		case 13: // Translated as Carriage Return
+			lastcrcol = column;
+			inc_row();
+			set_column();
+			if (curatr & 0xf0)
+				curatr = (curatr & ~0xff) | ((curatr & 0xf0) >> 4);
+			return true;
+		case 14: // Lower-case
+			return true;
+		case 15: // Flash on (C128 only)
+			if (subset == PETSCII_C128_80) {
+				curatr |= BLINK;
+				return true;
+			}
+			return false;
+		case 17: // Cursor down
+			inc_row();
+			return true;
+		case 19: // Home
+		case '\x93': // Clear
+			set_row();
+			set_column();
+			return true;
+		case 20: // Delete
+			if (column == 0) {
+				dec_row();
+				set_column(cols - 1);
+			}
+			else
+				dec_column();
+			return true;
+		case 29: // Cursor right
+			inc_column();
+			return true;
+		case '\x91': // Cursor up
+			dec_row();
+			return true;
+		case '\x9D': // Cursor Left
+			dec_column();
+			return true;
+
+		// Zero-width characters we want to pass through
+		case 2:
+			if (subset == PETSCII_C128_80) {
+				curatr |= UNDERLINE;
+				return true;
+			}
+			return false;
+		case 18: // Reverse on
+			if (!(curatr & REVERSED))
+				curatr = reverse_attr(curatr);
+			return true;
+		case '\x92': // Reverse off
+			if (curatr & REVERSED)
+				curatr = unreverse_attr(curatr);
+			return true;
+		case 5:  // White
+			set_color(WHITE);
+			return true;
+		case 28: // Red
+			set_color(RED);
+			return true;
+		case 30: // Green
+			set_color(GREEN);
+			return true;
+		case 31: // Blue
+			set_color(BLUE);
+			return true;
+		case '\x81': // Orange
+			set_color(MAGENTA);
+			return true;
+		case '\x82': // Underline off
+			if (subset == PETSCII_C128_80) {
+				curatr &= ~UNDERLINE;
+				return true;
+			}
+			return false;
+		case '\x8F': // Flash off (C128 only)
+			if (subset == PETSCII_C128_80) {
+				curatr &= ~BLINK;
+				return true;
+			}
+			return false;
+		case '\x90': // Black
+			set_color(BLACK);
+			return true;
+		case '\x95': // Brown
+			set_color(BROWN);
+			return true;
+		case '\x96': // Pink
+			set_color(LIGHTRED);
+			return true;
+		case '\x97': // Dark gray or Cyan
+			if (subset == PETSCII_C128_80)
+				set_color(CYAN);
+			else
+				set_color(DARKGRAY);
+			return true;
+		case '\x98': // Cyan or Gray
+			if (subset == PETSCII_C128_80)
+				set_color(DARKGRAY);
+			else
+				set_color(CYAN);
+			return true;
+		case '\x99': // Light Green
+			set_color(LIGHTGREEN);
+			return true;
+		case '\x9A': // Light Blue
+			set_color(LIGHTBLUE);
+			return true;
+		case '\x9B': // Light Gray
+			set_color(LIGHTGRAY);
+			return true;
+		case '\x9C': // Purple
+			set_color(LIGHTMAGENTA);
+			return true;
+		case '\x9E': // Yellow
+			set_color(YELLOW);
+			return true;
+		case '\x9F': // Cyan
+			set_color(LIGHTCYAN);
+			return true;
+		case '\x8E': // Upper case
+		case '\x94': // Insert
+			return true;
+
+		// Everything else is assumed one byte wide
+		default:
+			inc_column();
+			return true;
+	}
+	return false;
+}
+
+bool PETSCII_Terminal::can_highlight() { return true; }
+bool PETSCII_Terminal::can_move() { return true; }
+bool PETSCII_Terminal::is_monochrome() { return false; }
+
+void PETSCII_Terminal::updated() {
+	if (cols == 80)
+		subset = PETSCII_C128_80;
+	else
+		subset = PETSCII_C64;
+}
diff --git a/src/sbbs3/petscii_term.h b/src/sbbs3/petscii_term.h
new file mode 100644
index 0000000000000000000000000000000000000000..4b6933e8ed0200001be5c068be197084e3b25deb
--- /dev/null
+++ b/src/sbbs3/petscii_term.h
@@ -0,0 +1,50 @@
+#ifndef PETSCII_TERMINAL_H
+#define PETSCII_TERMINAL_H
+
+#include "sbbs.h"
+
+class PETSCII_Terminal : public Terminal {
+private:
+	void set_color(int c);
+	unsigned xlat_atr(unsigned atr);
+
+	unsigned saved_x{0};
+	unsigned saved_y{0};
+	// https://www.pagetable.com/c64ref/charset/
+	enum {
+		PETSCII_C64,
+		PETSCII_C128_80
+	} subset{PETSCII_C64};
+
+public:
+
+	PETSCII_Terminal() = delete;
+	using Terminal::Terminal;
+
+	virtual const char *attrstr(unsigned atr);
+	virtual char* attrstr(unsigned atr, unsigned curatr, char* str, size_t strsz);
+	virtual bool gotoxy(unsigned x, unsigned y);
+	virtual bool save_cursor_pos();
+	virtual bool restore_cursor_pos();
+	virtual void carriage_return();
+	virtual void line_feed(unsigned count = 1);
+	virtual void backspace(unsigned int count = 1);
+	virtual void newline(unsigned count = 1);
+	virtual void clearscreen();
+	virtual void cleartoeos();
+	virtual void cleartoeol();
+	virtual void clearline();
+	virtual void cursor_home();
+	virtual void cursor_up(unsigned count = 1);
+	virtual void cursor_down(unsigned count = 1);
+	virtual void cursor_right(unsigned count = 1);
+	virtual void cursor_left(unsigned count = 1);
+	virtual const char* type();
+	virtual bool parse_output(char ch);
+	virtual bool can_highlight();
+	virtual bool can_move();
+	virtual bool is_monochrome();
+	virtual void updated();
+};
+
+#endif
diff --git a/src/sbbs3/postmsg.cpp b/src/sbbs3/postmsg.cpp
index 4e8845dd2393b5fd829d12d8565a23af66a6ce60..23624007bb940a299a0c2e241c3a247475ea47b7 100644
--- a/src/sbbs3/postmsg.cpp
+++ b/src/sbbs3/postmsg.cpp
@@ -93,17 +93,17 @@ bool sbbs_t::postmsg(int subnum, int wm_mode, smb_t* resmb, smbmsg_t* remsg)
 	}
 
 	if (remsg) {
-		SAFECOPY_UTF8(title, msghdr_field(remsg, remsg->subj, NULL, term_supports(UTF8)));
+		SAFECOPY_UTF8(title, msghdr_field(remsg, remsg->subj, NULL, term->charset() == CHARSET_UTF8));
 		SAFECOPY(org_title, title);
 		if (remsg->hdr.attr & MSG_ANONYMOUS)
 			SAFECOPY(from, text[Anonymous]);
 		else
-			SAFECOPY_UTF8(from, msghdr_field(remsg, remsg->from, NULL, term_supports(UTF8)));
+			SAFECOPY_UTF8(from, msghdr_field(remsg, remsg->from, NULL, term->charset() == CHARSET_UTF8));
 		// If user posted this message, reply to the original recipient again
 		if (remsg->to != NULL
 		    && ((remsg->from_ext != NULL && atoi(remsg->from_ext) == useron.number)
 		        || stricmp(useron.alias, remsg->from) == 0 || stricmp(useron.name, remsg->from) == 0))
-			SAFECOPY_UTF8(touser, msghdr_field(remsg, remsg->to, NULL, term_supports(UTF8)));
+			SAFECOPY_UTF8(touser, msghdr_field(remsg, remsg->to, NULL, term->charset() == CHARSET_UTF8));
 		else
 			SAFECOPY_UTF8(touser, from);
 		if (remsg->to != NULL)
@@ -112,7 +112,7 @@ bool sbbs_t::postmsg(int subnum, int wm_mode, smb_t* resmb, smbmsg_t* remsg)
 		SAFECOPY(top, format_text(RegardingByToOn, remsg
 		                          , title
 		                          , from
-		                          , msghdr_field(remsg, remsg->to, NULL, term_supports(UTF8))
+		                          , msghdr_field(remsg, remsg->to, NULL, term->charset() == CHARSET_UTF8)
 		                          , timestr(smb_time(remsg->hdr.when_written))
 		                          , smb_zonestr(remsg->hdr.when_written.zone, NULL)));
 		if (remsg->tags != NULL)
diff --git a/src/sbbs3/prntfile.cpp b/src/sbbs3/prntfile.cpp
index caf470b4d592e6bde3d03d8168c089600c02f968..0b123d871624d34f10a79b0e9f892a65e6f58e8d 100644
--- a/src/sbbs3/prntfile.cpp
+++ b/src/sbbs3/prntfile.cpp
@@ -60,7 +60,7 @@ bool sbbs_t::printfile(const char* fname, int mode, int org_cols, JSObject* obj)
 	}
 
 	if (mode & P_NOABORT || rip) {
-		if (online == ON_REMOTE && console & CON_R_ECHO) {
+		if (online == ON_REMOTE) {
 			rioctl(IOCM | ABORT);
 			rioctl(IOCS | ABORT);
 		}
@@ -90,8 +90,8 @@ bool sbbs_t::printfile(const char* fname, int mode, int org_cols, JSObject* obj)
 	}
 
 	lprintf(LOG_DEBUG, "Printing file: %s", fpath);
-	if (!(mode & P_NOCRLF) && row > 0 && !rip) {
-		newline();
+	if (!(mode & P_NOCRLF) && term->row > 0 && !rip) {
+		term->newline();
 	}
 
 	if ((mode & P_OPENCLOSE) && length <= PRINTFILE_MAX_FILE_LEN) {
@@ -106,14 +106,14 @@ bool sbbs_t::printfile(const char* fname, int mode, int org_cols, JSObject* obj)
 			errormsg(WHERE, ERR_READ, fpath, length);
 		else {
 			buf[l] = 0;
-			if ((mode & P_UTF8) && !term_supports(UTF8))
+			if ((mode & P_UTF8) && (term->charset() != CHARSET_UTF8))
 				utf8_normalize_str(buf);
 			putmsg(buf, mode, org_cols, obj);
 		}
 		free(buf);
 	} else {    // Line-at-a-time mode
 		uint             sys_status_sav = sys_status;
-		enum output_rate output_rate = cur_output_rate;
+		enum output_rate output_rate = term->cur_output_rate;
 		uint             org_line_delay = line_delay;
 		uint             tmpatr = curatr;
 		uint             orgcon = console;
@@ -132,14 +132,17 @@ bool sbbs_t::printfile(const char* fname, int mode, int org_cols, JSObject* obj)
 		uint rainbow_sav[LEN_RAINBOW + 1];
 		memcpy(rainbow_sav, rainbow, sizeof rainbow_sav);
 
+		ansiParser.reset();
 		while (!feof(stream) && !msgabort()) {
 			if (fgets(buf, length + 1, stream) == NULL)
 				break;
-			if ((mode & P_UTF8) && !term_supports(UTF8))
+			if ((mode & P_UTF8) && (term->charset() != CHARSET_UTF8))
 				utf8_normalize_str(buf);
 			if (putmsgfrag(buf, mode, org_cols, obj) != '\0') // early-EOF?
 				break;
 		}
+		if (ansiParser.current_state() != ansiState_none)
+			lprintf(LOG_DEBUG, "Incomplete ANSI stripped from end");
 		memcpy(rainbow, rainbow_sav, sizeof rainbow);
 		free(buf);
 		fclose(stream);
@@ -147,10 +150,10 @@ bool sbbs_t::printfile(const char* fname, int mode, int org_cols, JSObject* obj)
 			console = orgcon;
 			attr(tmpatr);
 		}
-		if (!(mode & P_NOATCODES) && cur_output_rate != output_rate)
-			set_output_rate(output_rate);
+		if (!(mode & P_NOATCODES) && term->cur_output_rate != output_rate)
+			term->set_output_rate(output_rate);
 		if (mode & P_PETSCII)
-			outcom(PETSCII_UPPERLOWER);
+			term_out(PETSCII_UPPERLOWER);
 		attr_sp = 0;  /* clear any saved attributes */
 
 		/* Restore original settings of Forced Pause On/Off */
@@ -165,7 +168,7 @@ bool sbbs_t::printfile(const char* fname, int mode, int org_cols, JSObject* obj)
 		rioctl(IOSM | ABORT);
 	}
 	if (rip)
-		ansi_getdims();
+		term->getdims();
 	console = savcon;
 
 	return true;
@@ -208,8 +211,8 @@ bool sbbs_t::printtail(const char* fname, int lines, int mode, int org_cols, JSO
 	}
 
 	lprintf(LOG_DEBUG, "Printing tail: %s", fpath);
-	if (!(mode & P_NOCRLF) && row > 0) {
-		newline();
+	if (!(mode & P_NOCRLF) && term->row > 0) {
+		term->newline();
 	}
 
 	if (length > lines * PRINTFILE_MAX_LINE_LEN) {
@@ -273,17 +276,16 @@ bool sbbs_t::menu(const char *code, int mode, JSObject* obj)
 	if (menu_file[0])
 		SAFECOPY(path, menu_file);
 	else {
-		int term = term_supports();
 		do {
-			if ((term & RIP) && menu_exists(code, "rip", path))
+			if ((term->supports(RIP)) && menu_exists(code, "rip", path))
 				break;
-			if ((term & (ANSI | COLOR)) == ANSI && menu_exists(code, "mon", path))
+			if ((term->supports(ANSI) && (!term->supports(COLOR))) && menu_exists(code, "mon", path))
 				break;
-			if ((term & ANSI) && menu_exists(code, "ans", path))
+			if ((term->supports(ANSI)) && menu_exists(code, "ans", path))
 				break;
-			if ((term & PETSCII) && menu_exists(code, "seq", path))
+			if ((term->charset() == CHARSET_PETSCII) && menu_exists(code, "seq", path))
 				break;
-			if (term & NO_EXASCII) {
+			if (term->charset() == CHARSET_ASCII) {
 				next = "asc";
 				last = "msg";
 			}
@@ -298,7 +300,7 @@ bool sbbs_t::menu(const char *code, int mode, JSObject* obj)
 	}
 
 	mode |= P_OPENCLOSE | P_CPM_EOF;
-	if (column == 0)
+	if (term->column == 0)
 		mode |= P_NOCRLF;
 	return printfile(path, mode, /* org_cols: */ 0, obj);
 }
@@ -334,7 +336,7 @@ bool sbbs_t::menu_exists(const char *code, const char* ext, char* path)
 		SAFECOPY(prefix, path);
 	}
 	// Display specified EXACT width file
-	safe_snprintf(path, MAX_PATH, "%s.%ucol.%s", prefix, cols, ext);
+	safe_snprintf(path, MAX_PATH, "%s.%ucol.%s", prefix, term->cols, ext);
 	if (fexistcase(path))
 		return true;
 	// Display specified MINIMUM width file
@@ -345,12 +347,12 @@ bool sbbs_t::menu_exists(const char *code, const char* ext, char* path)
 		char   term[MAX_PATH + 1];
 		safe_snprintf(term, sizeof(term), ".%s", ext);
 		size_t skip = safe_snprintf(path, MAX_PATH, "%s.c", prefix);
-		int    max = 0;
+		unsigned max = 0;
 		for (size_t i = 0; i < g.gl_pathc; i++) {
-			int c = strtol(g.gl_pathv[i] + skip, &p, 10);
+			unsigned long c = strtoul(g.gl_pathv[i] + skip, &p, 10);
 			if (stricmp(p, term) != 0) // Some other weird pattern ending in c*.<ext>
 				continue;
-			if (c <= cols && c > max) {
+			if (c <= this->term->cols && c > max) {
 				max = c;
 				safe_snprintf(path, MAX_PATH, "%s", g.gl_pathv[i]);
 			}
diff --git a/src/sbbs3/putmsg.cpp b/src/sbbs3/putmsg.cpp
index bdbb7dcd2a42c52ff7c49e9629cd1a49cc75344a..d36318d124b42c2285a4b63ebc32e45b4e550411 100644
--- a/src/sbbs3/putmsg.cpp
+++ b/src/sbbs3/putmsg.cpp
@@ -42,7 +42,7 @@ char sbbs_t::putmsg(const char *buf, int mode, int org_cols, JSObject* obj)
 	uint             orgcon = console;
 	uint             sys_status_sav = sys_status;
 	uint             rainbow_sav[LEN_RAINBOW + 1];
-	enum output_rate output_rate = cur_output_rate;
+	enum output_rate output_rate = term->cur_output_rate;
 
 	attr_sp = 0;  /* clear any saved attributes */
 	tmpatr = curatr;  /* was lclatr(-1) */
@@ -52,17 +52,20 @@ char sbbs_t::putmsg(const char *buf, int mode, int org_cols, JSObject* obj)
 		sys_status |= SS_PAUSEOFF;
 
 	memcpy(rainbow_sav, rainbow, sizeof rainbow_sav);
+	ansiParser.reset();
 	char ret = putmsgfrag(buf, mode, org_cols, obj);
+	if (ansiParser.current_state() != ansiState_none)
+		lprintf(LOG_DEBUG, "Incomplete ANSI stripped from end");
 	memcpy(rainbow, rainbow_sav, sizeof rainbow);
 	if (!(mode & P_SAVEATR)) {
 		console = orgcon;
 		attr(tmpatr);
 	}
-	if (!(mode & P_NOATCODES) && cur_output_rate != output_rate)
-		set_output_rate(output_rate);
+	if (!(mode & P_NOATCODES) && term->cur_output_rate != output_rate)
+		term->set_output_rate(output_rate);
 
 	if (mode & P_PETSCII)
-		outcom(PETSCII_UPPERLOWER);
+		term_out(PETSCII_UPPERLOWER);
 
 	attr_sp = 0;  /* clear any saved attributes */
 
@@ -76,7 +79,7 @@ char sbbs_t::putmsg(const char *buf, int mode, int org_cols, JSObject* obj)
 }
 
 // Print a message fragment, doesn't save/restore any console states (e.g. attributes, auto-pause)
-char sbbs_t::putmsgfrag(const char* buf, int& mode, int org_cols, JSObject* obj)
+char sbbs_t::putmsgfrag(const char* buf, int& mode, unsigned org_cols, JSObject* obj)
 {
 	char                 tmp[256];
 	char                 tmp2[256];
@@ -85,17 +88,17 @@ char sbbs_t::putmsgfrag(const char* buf, int& mode, int org_cols, JSObject* obj)
 	uchar                exatr = 0;
 	char                 mark = '\0';
 	int                  i;
-	int                  col = column;
+	unsigned             col = term->column;
 	uint                 l = 0;
 	uint                 lines_printed = 0;
 	struct mouse_hotspot hot_spot = {};
+	bool                 lfisnl;
 
 	hot_attr = 0;
 	hungry_hotspots = true;
 	str = auto_utf8(str, mode);
 	size_t len = strlen(str);
 
-	int    term = term_supports();
 	if (!(mode & P_NOATCODES) && memcmp(str, "@WRAPOFF@", 9) == 0) {
 		mode &= ~P_WORDWRAP;
 		l += 9;
@@ -110,7 +113,7 @@ char sbbs_t::putmsgfrag(const char* buf, int& mode, int org_cols, JSObject* obj)
 		char *wrapped;
 		if (org_cols < TERM_COLS_MIN)
 			org_cols = TERM_COLS_DEFAULT;
-		if ((wrapped = ::wordwrap((char*)str + l, cols - 1, org_cols - 1, /* handle_quotes: */ TRUE
+		if ((wrapped = ::wordwrap((char*)str + l, term->cols - 1, org_cols - 1, /* handle_quotes: */ TRUE
 		                          , /* is_utf8: */ INT_TO_BOOL(mode & P_UTF8)
 		                          , /* pipe_codes: */(cfg.sys_misc & SM_RENEGADE) && !INT_TO_BOOL(mode & P_NOXATTRS))) == NULL)
 			errormsg(WHERE, ERR_ALLOC, "wordwrap buffer", 0);
@@ -139,18 +142,18 @@ char sbbs_t::putmsgfrag(const char* buf, int& mode, int org_cols, JSObject* obj)
 				}
 			// fallthrough
 			default: // printing char
-				if ((mode & P_INDENT) && column < col)
-					cursor_right(col - column);
-				else if ((mode & P_TRUNCATE) && column >= (cols - 1)) {
+				if ((mode & P_INDENT) && term->column < col)
+					term->cursor_right(col - term->column);
+				else if ((mode & P_TRUNCATE) && term->column >= (term->cols - 1)) {
 					l++;
 					continue;
 				} else if (mode & P_WRAP) {
 					if (org_cols) {
-						if (column > (org_cols - 1)) {
+						if (term->column > (org_cols - 1)) {
 							CRLF;
 						}
 					} else {
-						if (column >= (cols - 1)) {
+						if (term->column >= (term->cols - 1)) {
 							CRLF;
 						}
 					}
@@ -220,18 +223,18 @@ char sbbs_t::putmsgfrag(const char* buf, int& mode, int org_cols, JSObject* obj)
 			else if (str[l + 1] == '~') {
 				l += 2;
 				if (str[l] >= ' ')
-					add_hotspot(str[l], /* hungry: */ true);
+					term->add_hotspot(str[l], /* hungry: */ true);
 				else
-					add_hotspot('\r', /* hungry: */ true);
+					term->add_hotspot('\r', /* hungry: */ true);
 			}
 			else if (str[l + 1] == '`' && str[l + 2] >= ' ') {
-				add_hotspot(str[l + 2], /* hungry: */ false);
+				term->add_hotspot(str[l + 2], /* hungry: */ false);
 				l += 2;
 			}
 			else {
-				bool was_tos = (row == 0);
+				bool was_tos = (term->row == 0);
 				ctrl_a(str[l + 1]);
-				if (row == 0 && !was_tos && (sys_status & SS_ABORT) && !lines_printed) /* Aborted at (auto) pause prompt (e.g. due to CLS)? */
+				if (term->row == 0 && !was_tos && (sys_status & SS_ABORT) && !lines_printed) /* Aborted at (auto) pause prompt (e.g. due to CLS)? */
 					sys_status &= ~SS_ABORT;                /* Clear the abort flag (keep displaying the msg/file) */
 				l += 2;
 			}
@@ -375,26 +378,78 @@ char sbbs_t::putmsgfrag(const char* buf, int& mode, int org_cols, JSObject* obj)
 			l += 2;
 		}
 		else {
+			lfisnl = false;
 			if (!(mode & P_PETSCII) && str[l] == '\n') {
 				if (exatr)   /* clear at newline for extra attr codes */
 					attr(LIGHTGRAY);
 				if (l == 0 || str[l - 1] != '\r')  /* expand sole LF to CR/LF */
-					outchar('\r');
+					lfisnl = true;
 				lines_printed++;
 			}
 
-			/* ansi escape sequence */
-			if (outchar_esc >= ansiState_csi) {
-				if (str[l] == 'A' || str[l] == 'B' || str[l] == 'H' || str[l] == 'J'
-				    || str[l] == 'f' || str[l] == 'u')    /* ANSI anim */
-					lncntr = 0;         /* so defeat pause */
-				if (str[l] == '"' || str[l] == 'c') {
-					l++;                /* don't pass on keyboard reassignment or Device Attributes (DA) requests */
-					continue;
+			/*
+			 * ansi escape sequence:
+			 * Strip broken sequences
+			 * Strip "dangerous" sequences
+			 * Reset line counter on sequences that may change the row
+			 * (The last is done that way for backward compatibility)
+			 */
+			if (term->supports(ANSI)) {
+				switch (ansiParser.parse(str[l])) {
+					case ansiState_broken:
+						// TODO: Maybe just strip the CSI or something?
+						lprintf(LOG_DEBUG, "Stripping broken ANSI sequence \"%s\"", ansiParser.ansi_sequence.c_str());
+						ansiParser.reset();
+						// break here prints the first non-valid character
+						break;
+					case ansiState_none:
+						break;
+					case ansiState_final:
+						if ((!ansiParser.ansi_was_private) && ansiParser.ansi_final_byte == 'p')
+							lprintf(LOG_DEBUG, "Stripping SKR sequence");
+						else if (ansiParser.ansi_was_private && ansiParser.ansi_params[0] == '?' && ansiParser.ansi_final_byte == 'S')
+							lprintf(LOG_DEBUG, "Stripping XTSRGA sequence");
+						else if (ansiParser.ansi_final_byte == 'n')
+							lprintf(LOG_DEBUG, "Stripping DSR sequence");
+						else if (ansiParser.ansi_final_byte == 'c')
+							lprintf(LOG_DEBUG, "Stripping DA sequence");
+						else if (ansiParser.ansi_ibs == "," && ansiParser.ansi_final_byte == 'q')
+							lprintf(LOG_DEBUG, "Stripping DECTID sequence");
+						else if (ansiParser.ansi_ibs == "&" && ansiParser.ansi_final_byte == 'u')
+							lprintf(LOG_DEBUG, "Stripping DECRQUPSS sequence");
+						else if (ansiParser.ansi_ibs == "+" && ansiParser.ansi_final_byte == 'x')
+							lprintf(LOG_DEBUG, "Stripping DECRQPKFM sequence");
+						else if (ansiParser.ansi_ibs == "$" && ansiParser.ansi_final_byte == 'p')
+							lprintf(LOG_DEBUG, "Stripping DECRQM sequence");
+						else if (ansiParser.ansi_ibs == "$" && ansiParser.ansi_final_byte == 'u')
+							lprintf(LOG_DEBUG, "Stripping DECRQTSR sequence");
+						else if (ansiParser.ansi_ibs == "$" && ansiParser.ansi_final_byte == 'w')
+							lprintf(LOG_DEBUG, "Stripping DECRQPSR sequence");
+						else if (ansiParser.ansi_ibs == "*" && ansiParser.ansi_final_byte == 'y')
+							lprintf(LOG_DEBUG, "Stripping DECRQCRA sequence");
+						else if (ansiParser.ansi_sequence.substr(0, 4) == "\x1bP$q")
+							lprintf(LOG_DEBUG, "Stripping DECRQSS sequence");
+						else if (ansiParser.ansi_sequence.substr(0, 14) == "\x1b_SyncTERM:C;L")
+							lprintf(LOG_DEBUG, "Stripping CTSFI sequence");
+						else if (ansiParser.ansi_sequence.substr(0, 16) == "\x1b_SyncTERM:Q;JXL")
+							lprintf(LOG_DEBUG, "Stripping CTQJS sequence");
+						else {
+							if ((!ansiParser.ansi_was_private) && ansiParser.ansi_ibs == "") {
+								if (strchr("AFkBEeHfJdu", ansiParser.ansi_final_byte) != nullptr)    /* ANSI anim */
+									term->lncntr = 0; /* so defeat pause */
+							}
+							term_out(ansiParser.ansi_sequence.c_str(), ansiParser.ansi_sequence.length());
+						}
+						ansiParser.reset();
+						l++;
+						continue;
+					default:
+						l++;
+						continue;
 				}
 			}
 			if (str[l] == '!' && str[l + 1] == '|' && useron.misc & RIP) /* RIP */
-				lncntr = 0;             /* so defeat pause */
+				term->lncntr = 0;             /* so defeat pause */
 			if (str[l] == '@' && !(mode & P_NOATCODES)) {
 				if (memcmp(str + l, "@EOF@", 5) == 0)
 					break;
@@ -413,7 +468,7 @@ char sbbs_t::putmsgfrag(const char* buf, int& mode, int org_cols, JSObject* obj)
 						tmp[i++] = str[l++];
 					tmp[i] = 0;
 					truncsp(tmp);
-					center(expand_atcodes(tmp, tmp2, sizeof tmp2));
+					term->center(expand_atcodes(tmp, tmp2, sizeof tmp2));
 					if (str[l] == '\r')
 						l++;
 					if (str[l] == '\n')
@@ -468,10 +523,10 @@ char sbbs_t::putmsgfrag(const char* buf, int& mode, int org_cols, JSObject* obj)
 						continue;
 					}
 				}
-				bool was_tos = (row == 0);
+				bool was_tos = (term->row == 0);
 				i = show_atcode((char *)str + l, obj);  /* returns 0 if not valid @ code */
 				l += i;                   /* i is length of code string */
-				if (row > 0 && !was_tos && (sys_status & SS_ABORT) && !lines_printed)  /* Aborted at (auto) pause prompt (e.g. due to CLS)? */
+				if (term->row > 0 && !was_tos && (sys_status & SS_ABORT) && !lines_printed)  /* Aborted at (auto) pause prompt (e.g. due to CLS)? */
 					sys_status &= ~SS_ABORT;                /* Clear the abort flag (keep displaying the msg/file) */
 				if (i)                   /* if valid string, go to top */
 					continue;
@@ -480,69 +535,33 @@ char sbbs_t::putmsgfrag(const char* buf, int& mode, int org_cols, JSObject* obj)
 				break;
 			if (hot_attr) {
 				if (curatr == hot_attr && str[l] > ' ') {
-					hot_spot.y = row;
+					hot_spot.y = term->row;
 					if (!hot_spot.minx)
-						hot_spot.minx = column;
-					hot_spot.maxx = column;
+						hot_spot.minx = term->column;
+					hot_spot.maxx = term->column;
 					hot_spot.cmd[strlen(hot_spot.cmd)] = str[l];
 				} else if (hot_spot.cmd[0]) {
 					hot_spot.hungry = hungry_hotspots;
-					add_hotspot(&hot_spot);
+					term->add_hotspot(&hot_spot);
 					memset(&hot_spot, 0, sizeof(hot_spot));
 				}
 			}
 			size_t skip = sizeof(char);
 			if (mode & P_PETSCII) {
-				if (term & PETSCII) {
-					outcom(str[l]);
-					switch ((uchar)str[l]) {
-						case '\r':  // PETSCII "Return" / new-line
-							column = 0;
-						/* fall-through */
-						case PETSCII_DOWN:
-							lncntr++;
-							break;
-						case PETSCII_CLEAR:
-						case PETSCII_HOME:
-							row = 0;
-							column = 0;
-							lncntr = 0;
-							break;
-						case PETSCII_BLACK:
-						case PETSCII_WHITE:
-						case PETSCII_RED:
-						case PETSCII_GREEN:
-						case PETSCII_BLUE:
-						case PETSCII_ORANGE:
-						case PETSCII_BROWN:
-						case PETSCII_YELLOW:
-						case PETSCII_CYAN:
-						case PETSCII_LIGHTRED:
-						case PETSCII_DARKGRAY:
-						case PETSCII_MEDIUMGRAY:
-						case PETSCII_LIGHTGREEN:
-						case PETSCII_LIGHTBLUE:
-						case PETSCII_LIGHTGRAY:
-						case PETSCII_PURPLE:
-						case PETSCII_UPPERLOWER:
-						case PETSCII_UPPERGRFX:
-						case PETSCII_FLASH_ON:
-						case PETSCII_FLASH_OFF:
-						case PETSCII_REVERSE_ON:
-						case PETSCII_REVERSE_OFF:
-							// No cursor movement
-							break;
-						default:
-							inc_column(1);
-							break;
-					}
+				if (term->charset() == CHARSET_PETSCII) {
+					term_out(str[l]);
 				} else
 					petscii_to_ansibbs(str[l]);
 			} else if ((str[l] & 0x80) && (mode & P_UTF8)) {
-				if (term & UTF8)
-					outcom(str[l]);
+				if (term->charset() == CHARSET_UTF8)
+					term_out(str[l]);
 				else
 					skip = print_utf8_as_cp437(str + l, len - l);
+			} else if (str[l] == '\r' && str[l + 1] == '\n') {
+				term->newline();
+				skip++;
+			} else if (str[l] == '\n' && lfisnl) {
+				term->newline();
 			} else {
 				uint atr = curatr;
 				outchar(str[l]);
diff --git a/src/sbbs3/qwk.cpp b/src/sbbs3/qwk.cpp
index e9941739be0de0fdced1cb73ff961e959a6a8552..0428e6962f277fd5b9da889f2c9601480601b53f 100644
--- a/src/sbbs3/qwk.cpp
+++ b/src/sbbs3/qwk.cpp
@@ -436,63 +436,63 @@ void sbbs_t::qwk_sec()
 			while (online) {
 				CLS;
 				bprintf(text[QWKSettingsHdr], useron.alias, useron.number);
-				add_hotspot('A');
+				term->add_hotspot('A');
 				bprintf(text[QWKSettingsCtrlA]
 				        , useron.qwk & QWK_EXPCTLA
 				    ? "Expand to ANSI" : useron.qwk & QWK_RETCTLA ? "Leave in"
 				    : "Strip");
-				add_hotspot('T');
+				term->add_hotspot('T');
 				bprintf(text[QWKSettingsArchive], useron.tmpext);
-				add_hotspot('E');
+				term->add_hotspot('E');
 				bprintf(text[QWKSettingsEmail]
 				        , useron.qwk & QWK_EMAIL ? "Un-read Only"
 				    : useron.qwk & QWK_ALLMAIL ? text[Yes] : text[No]);
 				if (useron.qwk & (QWK_ALLMAIL | QWK_EMAIL)) {
-					add_hotspot('I');
+					term->add_hotspot('I');
 					bprintf(text[QWKSettingsAttach]
 					        , useron.qwk & QWK_ATTACH ? text[Yes] : text[No]);
-					add_hotspot('D');
+					term->add_hotspot('D');
 					bprintf(text[QWKSettingsDeleteEmail]
 					        , useron.qwk & QWK_DELMAIL ? text[Yes]:text[No]);
 				}
-				add_hotspot('F');
+				term->add_hotspot('F');
 				bprintf(text[QWKSettingsNewFilesList]
 				        , useron.qwk & QWK_FILES ? text[Yes]:text[No]);
-				add_hotspot('N');
+				term->add_hotspot('N');
 				bprintf(text[QWKSettingsIndex]
 				        , useron.qwk & QWK_NOINDEX ? text[No]:text[Yes]);
-				add_hotspot('C');
+				term->add_hotspot('C');
 				bprintf(text[QWKSettingsControl]
 				        , useron.qwk & QWK_NOCTRL ? text[No]:text[Yes]);
-				add_hotspot('V');
+				term->add_hotspot('V');
 				bprintf(text[QWKSettingsVoting]
 				        , useron.qwk & QWK_VOTING ? text[Yes]:text[No]);
-				add_hotspot('H');
+				term->add_hotspot('H');
 				bprintf(text[QWKSettingsHeaders]
 				        , useron.qwk & QWK_HEADERS ? text[Yes]:text[No]);
-				add_hotspot('Y');
+				term->add_hotspot('Y');
 				bprintf(text[QWKSettingsBySelf]
 				        , useron.qwk & QWK_BYSELF ? text[Yes]:text[No]);
-				add_hotspot('Z');
+				term->add_hotspot('Z');
 				bprintf(text[QWKSettingsTimeZone]
 				        , useron.qwk & QWK_TZ ? text[Yes]:text[No]);
-				add_hotspot('P');
+				term->add_hotspot('P');
 				bprintf(text[QWKSettingsVIA]
 				        , useron.qwk & QWK_VIA ? text[Yes]:text[No]);
-				add_hotspot('M');
+				term->add_hotspot('M');
 				bprintf(text[QWKSettingsMsgID]
 				        , useron.qwk & QWK_MSGID ? text[Yes]:text[No]);
-				add_hotspot('U');
+				term->add_hotspot('U');
 				bprintf(text[QWKSettingsUtf8]
 				        , useron.qwk & QWK_UTF8 ? text[Yes]:text[No]);
-				add_hotspot('W');
+				term->add_hotspot('W');
 				bprintf(text[QWKSettingsWrapText]
 				        , useron.qwk & QWK_WORDWRAP ? text[Yes]:text[No]);
-				add_hotspot('X');
+				term->add_hotspot('X');
 				bprintf(text[QWKSettingsExtended]
 				        , useron.qwk & QWK_EXT ? text[Yes]:text[No]);
 				bputs(text[QWKSettingsWhich]);
-				add_hotspot('Q');
+				term->add_hotspot('Q');
 				ch = (char)getkeys("AEDFHIOPQTUYMNCXZVW", 0);
 				if (sys_status & SS_ABORT || !ch || ch == 'Q' || !online)
 					break;
@@ -581,7 +581,7 @@ void sbbs_t::qwk_sec()
 				putuserqwk(useron.number, useron.qwk);
 			}
 			delfiles(cfg.temp_dir, ALLFILES);
-			clear_hotspots();
+			term->clear_hotspots();
 			continue;
 		}
 
diff --git a/src/sbbs3/readmail.cpp b/src/sbbs3/readmail.cpp
index dadd798aaed058e6d310c60ab3b31064a8dbcf53..925a95ee13c7f6998c615b8657bf35af084d5245 100644
--- a/src/sbbs3/readmail.cpp
+++ b/src/sbbs3/readmail.cpp
@@ -653,14 +653,14 @@ int sbbs_t::readmail(uint usernumber, int which, int lm_mode)
 				break;
 			case TERM_KEY_HOME:
 				smb.curmsg = 0;
-				newline();
+				term->newline();
 				break;
 			case TERM_KEY_END:
 				smb.curmsg = smb.msgs - 1;
-				newline();
+				term->newline();
 				break;
 			case TERM_KEY_RIGHT:
-				newline();
+				term->newline();
 			// fall-through
 			case 0:
 			case '+':
@@ -670,7 +670,7 @@ int sbbs_t::readmail(uint usernumber, int which, int lm_mode)
 					done = 1;
 				break;
 			case TERM_KEY_LEFT:
-				newline();
+				term->newline();
 			// fall-through
 			case '-':
 				if (smb.curmsg > 0)
diff --git a/src/sbbs3/readmsgs.cpp b/src/sbbs3/readmsgs.cpp
index d725251b016d31e52de7d69affc4377c9b811545..edb530058308048cf3c588d2da6e33ceb4f766b9 100644
--- a/src/sbbs3/readmsgs.cpp
+++ b/src/sbbs3/readmsgs.cpp
@@ -97,7 +97,7 @@ int sbbs_t::listmsgs(int subnum, int mode, post_t *post, int start, int posts, b
 
 void sbbs_t::dump_msghdr(smbmsg_t* msg)
 {
-	newline();
+	term->newline();
 	str_list_t list = smb_msghdr_str_list(msg);
 	if (list != NULL) {
 		for (int i = 0; list[i] != NULL && !msgabort(); i++) {
@@ -379,8 +379,8 @@ void sbbs_t::show_thread(uint32_t msgnum, post_t* post, unsigned curmsg, int thr
 //		,msg.hdr.number
 	        , (unsigned)i == curmsg ? '>' : ':');
 	bprintf("\1w%-*.*s\1g%c\1g%c \1w%s\r\n"
-	        , (int)(cols - column - 12)
-	        , (int)(cols - column - 12)
+	        , (int)(term->cols - term->column - 12)
+	        , (int)(term->cols - term->column - 12)
 	        , msg.hdr.attr & MSG_ANONYMOUS && !sub_op(smb.subnum)
 	        ? text[Anonymous] : msghdr_field(&msg, msg.from)
 	        , (unsigned)i == curmsg ? '<' : ' '
@@ -447,7 +447,7 @@ int sbbs_t::scanposts(int subnum, int mode, const char *find)
 	}
 	ZERO_VAR(msg);              /* init to NULL, specify not-allocated */
 	if (!(mode & SCAN_CONT))
-		lncntr = 0;
+		term->lncntr = 0;
 	if ((msgs = getlastmsg(subnum, &last, 0)) == 0) {
 		if (mode & (SCAN_NEW | SCAN_TOYOU))
 			bprintf(text[NScanStatusFmt]
@@ -496,7 +496,7 @@ int sbbs_t::scanposts(int subnum, int mode, const char *find)
 		for (smb.curmsg = 0; smb.curmsg < smb.msgs; smb.curmsg++)
 			if (subscan[subnum].ptr < post[smb.curmsg].idx.number)
 				break;
-		lncntr = 0;
+		term->lncntr = 0;
 		bprintf(text[NScanStatusFmt]
 		        , cfg.grp[cfg.sub[subnum]->grp]->sname, cfg.sub[subnum]->lname, smb.msgs - smb.curmsg, msgs);
 		if (!smb.msgs) {       /* no messages at all */
@@ -516,8 +516,8 @@ int sbbs_t::scanposts(int subnum, int mode, const char *find)
 		}
 	}
 	else {
-		cleartoeol();
-		lncntr = 0;
+		term->cleartoeol();
+		term->lncntr = 0;
 		if (mode & SCAN_TOYOU)
 			bprintf(text[NScanStatusFmt]
 			        , cfg.grp[cfg.sub[subnum]->grp]->sname, cfg.sub[subnum]->lname, smb.msgs, msgs);
@@ -649,7 +649,7 @@ int sbbs_t::scanposts(int subnum, int mode, const char *find)
 				break;
 			}
 			bprintf("\1n\1l\1h\1bThread\1n\1b: \1h\1c");
-			bprintf("%-.*s\r\n", (int)(cols - (column + 1)), msghdr_field(&msg, msg.subj));
+			bprintf("%-.*s\r\n", (int)(term->cols - (term->column + 1)), msghdr_field(&msg, msg.subj));
 			show_thread(first, post, smb.curmsg);
 			subscan[subnum].last = post[smb.curmsg].idx.number;
 		}
@@ -783,8 +783,8 @@ int sbbs_t::scanposts(int subnum, int mode, const char *find)
 		sync();
 		if (unvalidated < smb.curmsg)
 			bprintf(text[UnvalidatedWarning], unvalidated + 1);
-		if (lncntr >= rows - 2)
-			lncntr--;
+		if (term->lncntr >= term->rows - 2)
+			term->lncntr--;
 		bprintf(text[ReadingSub], ugrp, cfg.grp[cfg.sub[subnum]->grp]->sname
 		        , usub, cfg.sub[subnum]->sname, smb.curmsg + 1, smb.msgs);
 		snprintf(str, sizeof str, "ABCDEFHILMNPQRTUVY?*<>[]{}-+()\b%c%c%c%c"
@@ -924,7 +924,7 @@ int sbbs_t::scanposts(int subnum, int mode, const char *find)
 							SAFEPRINTF2(str, "removed post from %s %s"
 							            , cfg.grp[cfg.sub[subnum]->grp]->sname, cfg.sub[subnum]->lname);
 							logline("P-", str);
-							center(text[Deleted]);
+							term->center(text[Deleted]);
 							if (!stricmp(cfg.sub[subnum]->misc & SUB_NAME
 							    ? useron.name : useron.alias, msg.from))
 								useron.posts = (ushort)adjustuserval(&cfg, useron.number, USER_POSTS, -1);
@@ -1387,7 +1387,7 @@ int sbbs_t::scanposts(int subnum, int mode, const char *find)
 			{
 				if (!thread_mode) {
 					smb.curmsg = 0;
-					newline();
+					term->newline();
 					break;
 				}
 				uint32_t first = smb_first_in_thread(&smb, &msg, NULL);
@@ -1405,7 +1405,7 @@ int sbbs_t::scanposts(int subnum, int mode, const char *find)
 			{
 				if (!thread_mode) {
 					smb.curmsg = smb.msgs - 1;
-					newline();
+					term->newline();
 					break;
 				}
 				uint32_t last = smb_last_in_thread(&smb, &msg);
@@ -1449,7 +1449,7 @@ int sbbs_t::scanposts(int subnum, int mode, const char *find)
 						smb.curmsg++;
 					else
 						done = 1;
-					newline();
+					term->newline();
 					break;
 				}
 				l = msg.hdr.thread_first;
@@ -1475,7 +1475,7 @@ int sbbs_t::scanposts(int subnum, int mode, const char *find)
 				if (!thread_mode) {
 					if (smb.curmsg > 0)
 						smb.curmsg--;
-					newline();
+					term->newline();
 					break;
 				}
 			case '(':   /* Thread backwards */
diff --git a/src/sbbs3/sbbs.h b/src/sbbs3/sbbs.h
index d2d60895d9964657f8765fd6243da44af0918f44..c94b08a2e41294d05e81eede4c581f49af719e74 100644
--- a/src/sbbs3/sbbs.h
+++ b/src/sbbs3/sbbs.h
@@ -276,6 +276,7 @@ extern int	thread_suid_broken;			/* NPTL is no longer broken */
 #include "startup.h"
 #ifdef __cplusplus
 	#include "threadwrap.h"	/* pthread_mutex_t */
+	#include "ansi_parser.h"
 #endif
 
 #include "smblib.h"
@@ -403,13 +404,7 @@ typedef struct js_callback {
 #include <string>
 #include <unordered_map>
 
-struct mouse_hotspot {		// Mouse hot-spot
-	char	cmd[128];
-	int	y;
-	int	minx;
-	int	maxx;
-	bool	hungry;
-};
+class Terminal;
 
 enum sftp_dir_tree {
 	SFTP_DTREE_FULL,
@@ -478,23 +473,14 @@ public:
 
 	std::atomic<bool> ssh_mode{false};
 	bool term_output_disabled{};
-	SOCKET	passthru_socket=INVALID_SOCKET;
-	bool	passthru_socket_active = false;
-	void	passthru_socket_activate(bool);
-    bool	passthru_thread_running = false;
+	SOCKET passthru_socket=INVALID_SOCKET;
+	bool   passthru_socket_active = false;
+	void   passthru_socket_activate(bool);
+	bool   passthru_thread_running = false;
 
 	scfg_t	cfg{};
 	struct mqtt* mqtt = nullptr;
-
-	enum ansiState {
-		 ansiState_none		// No sequence
-		,ansiState_esc		// Escape
-		,ansiState_csi		// CSI
-		,ansiState_final	// Final byte
-		,ansiState_string	// APS, DCS, PM, or OSC
-		,ansiState_sos		// SOS
-		,ansiState_sos_esc	// ESC inside SOS
-	} outchar_esc = ansiState_none;	// track ANSI escape seq output
+	Terminal *term{nullptr};
 
 	int 	rioctl(ushort action); // remote i/o control
 	bool	rio_abortable = false;
@@ -606,7 +592,7 @@ public:
 	char	prev_key(void) { return toupper(*text[Previous]); }
 
 	char 	dszlog[127]{};	/* DSZLOG environment variable */
-    int     keybuftop=0, keybufbot=0;    /* Keyboard input buffer pointers (for ungetkey) */
+	int     keybuftop=0, keybufbot=0;    /* Keyboard input buffer pointers (for ungetkey) */
 	char    keybuf[KEY_BUFSIZE]{};    /* Keyboard input buffer */
 	size_t	keybuf_space(void);
 	size_t	keybuf_level(void);
@@ -621,7 +607,7 @@ public:
 	uint	socket_inactive=0;			// Socket inactivity counter (watchdog), in seconds, incremented by input_thread()
 	uint	max_socket_inactivity=0;	// Socket inactivity limit (in seconds), enforced by input_thread()
 	bool	socket_inactivity_warning_sent=false;
-	uint	curatr = LIGHTGRAY;	/* Current Text Attributes Always */
+	uint	curatr = LIGHTGRAY;     /* Last Attributes requested by attr() */
 	uint	attr_stack[64]{};	/* Saved attributes (stack) */
 	int 	attr_sp = 0;	/* Attribute stack pointer */
 	uint	mneattr_low = LIGHTGRAY;
@@ -630,22 +616,10 @@ public:
 	uint	rainbow[LEN_RAINBOW + 1]{};
 	bool	rainbow_repeat = false;
 	int		rainbow_index = -1;
-	int 	lncntr = 0; 	/* Line Counter - for PAUSE */
 	bool	msghdr_tos = false;	/* Message header was displayed at Top of Screen */
-	int		row=0;			/* Current row */
-	int 	rows=0;			/* Current number of Rows for User */
-	int		cols=0;			/* Current number of Columns for User */
-	int		column = 0;		/* Current column counter (for line counter) */
-	int		tabstop = 8;	/* Current symmetric-tabstop (size) */
-	int		lastlinelen = 0;	/* The previously displayed line length */
 	int 	autoterm=0;		/* Auto-detected terminal type */
 	size_t	unicode_zerowidth=0;
 	char	terminal[TELNET_TERM_MAXLEN+1]{};	// <- answer() writes to this
-	int		cterm_version=0;/* (MajorVer*1000) + MinorVer */
-	link_list_t savedlines{};
-	char 	lbuf[LINE_BUFSIZE+1]{};/* Temp storage for each line output */
-	int		lbuflen = 0;	/* Number of characters in line buffer */
-	uint	latr=0;			/* Starting attribute of line buffer */
 	uint	line_delay=0;	/* Delay duration (ms) after each line sent */
 	uint	console = 0;	/* Defines current Console settings */
 	char 	wordwrap[TERM_COLS_MAX + 1]{};	/* Word wrap buffer */
@@ -722,23 +696,6 @@ public:
 	long	sysvar_l[MAX_SYSVARS]{};
 	uint	sysvar_li = 0;
 
-    /* ansi_term.cpp */
-	const char*	ansi(int atr);			/* Returns ansi escape sequence for atr */
-	char*	ansi(int atr, int curatr, char* str);
-    bool	ansi_gotoxy(int x, int y);
-	bool	ansi_getxy(int* x, int* y);
-	bool	ansi_save(void);
-	bool	ansi_restore(void);
-	bool	ansi_getdims(void);
-	enum ansi_mouse_mode {
-		ANSI_MOUSE_X10	= 9,
-		ANSI_MOUSE_NORM	= 1000,
-		ANSI_MOUSE_BTN	= 1002,
-		ANSI_MOUSE_ANY	= 1003,
-		ANSI_MOUSE_EXT	= 1006
-	};
-	int		ansi_mouse(enum ansi_mouse_mode, bool enable);
-
 			/* Command Shell Methods */
 	int		exec(csi_t *csi);
 	int		exec_function(csi_t *csi);
@@ -853,9 +810,10 @@ public:
 
 	/* putmsg.cpp */
 	char	putmsg(const char *str, int mode, int org_cols = 0, JSObject* obj = NULL);
-	char	putmsgfrag(const char* str, int& mode, int org_cols = 0, JSObject* obj = NULL);
+	char	putmsgfrag(const char* str, int& mode, unsigned org_cols = 0, JSObject* obj = NULL);
 	bool	putnmsg(int node_num, const char*);
 	bool	putsmsg(int user_num, const char*);
+	ANSI_Parser ansiParser{};
 
 	/* writemsg.cpp */
 	void	automsg(void);
@@ -917,10 +875,9 @@ public:
 	int		bulkmailhdr(smb_t*, smbmsg_t*, uint usernum);
 
 	/* con_out.cpp */
-	size_t	bstrlen(const char *str, int mode = 0);
 	int		bputs(const char *str, int mode = 0);	/* BBS puts function */
 	int		bputs(int mode, const char* str) { return bputs(str, mode); }
-	int		rputs(const char *str, size_t len=0);	/* BBS raw puts function */
+	size_t	rputs(const char *str, size_t len=0);	/* BBS raw puts function */
 	int		rputs(int mode, const char* str) { return rputs(str, mode); }
 	int		bprintf(const char *fmt, ...)			/* BBS printf function */
 #if defined(__GNUC__)   // Catch printf-format errors
@@ -937,37 +894,17 @@ public:
     __attribute__ ((format (printf, 2, 3)));		// 1 is 'this'
 #endif
 	;
-	int		comprintf(const char *fmt, ...)			/* BBS direct-comm printf function */
+	int		term_printf(const char *fmt, ...)			/* BBS direct-comm printf function */
 #if defined(__GNUC__)   // Catch printf-format errors
     __attribute__ ((format (printf, 2, 3)));		// 1 is 'this'
 #endif
 	;
-	void	backspace(int count=1);			/* Output destructive backspace(s) via outchar */
 	int		outchar(char ch);				/* Output a char - check echo and emu.  */
-	int		outchar(enum unicode_codepoint, char cp437_fallback);
-	int		outchar(enum unicode_codepoint, const char* cp437_fallback = NULL);
-	void	inc_row(int count);
-	void	inc_column(int count);
-	void	center(const char *str, bool msg = false, unsigned int columns = 0);
+	bool	check_pause();			/* Check lncntr to and pause() if appropriate */
+	int		outcp(enum unicode_codepoint, char cp437_fallback);
+	int		outcp(enum unicode_codepoint, const char* cp437_fallback = NULL);
 	void	wide(const char*);
-	void	clearscreen(int term);
-	void	clearline(void);
-	void	cleartoeol(void);
-	void	cleartoeos(void);
-	void	cursor_home(void);
-	void	cursor_up(int count=1);
-	void	cursor_down(int count=1);
-	void	cursor_left(int count=1);
-	void	cursor_right(int count=1);
-	bool	cursor_xy(int x, int y);
-	bool	cursor_getxy(int* x, int* y);
-	void	carriage_return(int count=1);
-	void	line_feed(int count=1);
-	void	newline(int count=1);
-	void	cond_newline() { if(column > 0) newline(); }
-	void	cond_blankline() { if(column > 0) newline(); if(lastlinelen) newline(); }
-	void	cond_contline() { if(column > 0 && cols < TERM_COLS_DEFAULT) bputs(text[LongLineContinuationPrefix]); }
-	int		term_supports(int cmp_flags=0);
+	// These are user settings, not terminal properties.
 	char*	term_rows(user_t*, char* str, size_t);
 	char*	term_cols(user_t*, char* str, size_t);
 	char*	term_type(user_t*, int term, char* str, size_t);
@@ -977,35 +914,21 @@ public:
 	int		backfill(const char* str, float pct, int full_attr, int empty_attr);
 	void	progress(const char* str, int count, int total, int interval = 500);
 	double	last_progress = 0;
-	bool	saveline(void);
-	bool	restoreline(void);
 	int		petscii_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 */
 	char*	auto_utf8(const char*, int& mode);
-	enum output_rate {
-		output_rate_unlimited,
-		output_rate_300 = 300,
-		output_rate_600 = 600,
-		output_rate_1200 = 1200,
-		output_rate_2400 = 2400,
-		output_rate_4800 = 4800,
-		output_rate_9600 = 9600,
-		output_rate_19200 = 19200,
-		output_rate_38400 = 38400,
-		output_rate_57600 = 57600,
-		output_rate_76800 = 76800,
-		output_rate_115200 = 115200,
-	} cur_output_rate = output_rate_unlimited;
-	void	set_output_rate(enum output_rate);
 	void	getdimensions();
+	size_t term_out(const char *str, size_t len = SIZE_MAX);
+	size_t term_out(int ch);
+	size_t cp437_out(const char *str, size_t len = SIZE_MAX);
+	size_t cp437_out(int ch);
 
 	/* getstr.cpp */
 	size_t	getstr_offset = 0;
 	size_t	getstr(char *str, size_t length, int mode, const str_list_t history = NULL);
 	int		getnum(uint max, uint dflt=0);
-	void	insert_indicator(void);
 
 	/* getkey.cpp */
 	char	getkey(int mode = K_NONE);
@@ -1021,6 +944,7 @@ public:
 	void	mnemonics(const char *str);
 
 	/* inkey.cpp */
+	bool last_inkey_was_esc{false}; // Used by auto-ANSI detection
 	int		inkey(int mode = K_NONE, unsigned int timeout=0);
 	char	handle_ctrlkey(char ch, int mode=0);
 
@@ -1036,16 +960,6 @@ public:
 	int		mouse_mode = MOUSE_MODE_OFF;	// Mouse reporting mode flags
 	uint	hot_attr = 0;		// Auto-Mouse hot-spot attribute (when non-zero)
 	bool	hungry_hotspots = true;
-	link_list_t mouse_hotspots{};	// Mouse hot-spots
-	struct mouse_hotspot* pause_hotspot = nullptr;
-	struct mouse_hotspot* add_hotspot(struct mouse_hotspot*);
-	struct mouse_hotspot* add_hotspot(char cmd, bool hungry = true, int minx = -1, int maxx = -1, int y = -1);
-	struct mouse_hotspot* add_hotspot(int num, bool hungry = true, int minx = -1, int maxx = -1, int y = -1);
-	struct mouse_hotspot* add_hotspot(uint num, bool hungry = true, int minx = -1, int maxx = -1, int y = -1);
-	struct mouse_hotspot* add_hotspot(const char* cmd, bool hungry = true, int minx = -1, int maxx = -1, int y = -1);
-	void	clear_hotspots(void);
-	void	scroll_hotspots(int count);
-	void	set_mouse(int mode);
 
 	// Thread-safe std/socket errno description getters
 	char	strerror_buf[256]{};
@@ -1361,6 +1275,8 @@ public:
 
 };
 
+#include "terminal.h"
+
 #endif /* __cplusplus */
 
 #ifdef DLLEXPORT
@@ -1383,8 +1299,6 @@ public:
 #ifdef __cplusplus
 extern "C" {
 #endif
-	/* ansiterm.cpp */
-	DLLEXPORT char*		ansi_attr(int attr, int curattr, char* str, bool color);
 
 	/* main.cpp */
 	extern const char* nulstr;
diff --git a/src/sbbs3/sbbs.jsdocs.vcxproj b/src/sbbs3/sbbs.jsdocs.vcxproj
index 1291236adc1a70438e0fb7bbb206e04dc87c4509..38fbfc21794e0c1ad459d8d1579d07ca7016f7bc 100644
--- a/src/sbbs3/sbbs.jsdocs.vcxproj
+++ b/src/sbbs3/sbbs.jsdocs.vcxproj
@@ -187,12 +187,6 @@
     <ClCompile Include="..\encode\utf8.c" />
     <ClCompile Include="..\encode\uucode.c" />
     <ClCompile Include="..\encode\yenc.c" />
-    <ClCompile Include="ansiterm.cpp">
-      <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
-      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
-      <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
-      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
-    </ClCompile>
     <ClCompile Include="answer.cpp">
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
@@ -833,4 +827,4 @@
   <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
   <ImportGroup Label="ExtensionTargets">
   </ImportGroup>
-</Project>
\ No newline at end of file
+</Project>
diff --git a/src/sbbs3/sbbs.vcxproj b/src/sbbs3/sbbs.vcxproj
index de2979125cb562825c35bb8c4cb4570bab36affa..a328b73f2bfcef365d4aa60b735aa7a1594526d2 100644
--- a/src/sbbs3/sbbs.vcxproj
+++ b/src/sbbs3/sbbs.vcxproj
@@ -185,12 +185,8 @@
     <ClCompile Include="..\encode\utf8.c" />
     <ClCompile Include="..\encode\uucode.c" />
     <ClCompile Include="..\encode\yenc.c" />
-    <ClCompile Include="ansiterm.cpp">
-      <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
-      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
-      <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
-      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
-    </ClCompile>
+    <ClCompile Include="ansi_parser.cpp" />
+    <ClCompile Include="ansi_terminal.cpp" />
     <ClCompile Include="answer.cpp">
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
@@ -596,6 +592,7 @@
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
     </ClCompile>
+    <ClCompile Include="petscii_term.cpp" />
     <ClCompile Include="postmsg.cpp">
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
@@ -719,6 +716,7 @@
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
     </ClCompile>
+    <ClCompile Include="terminal.cpp" />
     <ClCompile Include="text_defaults.c">
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
@@ -823,4 +821,4 @@
   <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
   <ImportGroup Label="ExtensionTargets">
   </ImportGroup>
-</Project>
\ No newline at end of file
+</Project>
diff --git a/src/sbbs3/sbbsdefs.h b/src/sbbs3/sbbsdefs.h
index 9b78f21f3df11b5d0984ccdbd6d89b40e15fa27e..78e8a5caf608c5df74e14477285ca153b824fa7a 100644
--- a/src/sbbs3/sbbsdefs.h
+++ b/src/sbbs3/sbbsdefs.h
@@ -476,11 +476,11 @@ typedef enum {                      /* Values for xtrn_t.event				*/
 #define EDIT_TABSIZE 4      /* Tab size for internal message/line editor	*/
 
 /* Console I/O Bits	(console)				*/
-#define CON_R_ECHO      (1 << 0)  /* Echo remotely							*/
-#define CON_R_ECHOX     (1 << 1)  /* Echo X's to remote user					*/
+#define CON_R_ECHO      0         /* Echo remotely - Unused					*/
+#define CON_R_ECHOX     (1 << 1)  /* Echo X's to remote user				*/
 #define CON_L_ECHOX     0       // Unused
 #define CON_R_INPUT     (1 << 2)  /* Accept input remotely					*/
-#define CON_L_ECHO      (1 << 3)  /* Echo locally              				*/
+#define CON_L_ECHO      0         /* Echo locally              				*/
 #define CON_PAUSEOFF    (1 << 4)  // Temporary pause over-ride (same as UPAUSE)
 #define CON_L_INPUT     (1 << 5)  /* Accept input locally						*/
 #define CON_RAW_IN      (1 << 8)  /* Raw input mode - no editing capabilities	*/
@@ -578,7 +578,9 @@ typedef enum {                      /* Values for xtrn_t.event				*/
 #define UTF8        (1 << 29)     /* UTF-8 terminal						*/
 #define MOUSE       (1U << 31)    /* Mouse supported terminal				*/
 
+// TODO: Really, NO_EXASCII  and UTF8 are not terminal flags.
 #define TERM_FLAGS      (ANSI | COLOR | RIP | SWAP_DELETE | ICE_COLOR | MOUSE | CHARSET_FLAGS)
+// TODO: Picking these out gets tricky, PETSCII is both terminal and charset
 #define CHARSET_FLAGS   (NO_EXASCII | PETSCII | UTF8)
 #define CHARSET_ASCII   NO_EXASCII  // US-ASCII
 #define CHARSET_PETSCII PETSCII     // CBM-ASCII
@@ -847,7 +849,7 @@ enum {                          /* Values of mode for userlist function     */
 /* Macros */
 /**********/
 
-#define CRLF            newline()
+#define CRLF            term->newline()
 #define SYSOP_LEVEL     90
 #define SYSOP           (useron.level >= SYSOP_LEVEL || sys_status & SS_TMPSYSOP)
 #define REALSYSOP       (useron.level >= SYSOP_LEVEL)
@@ -891,9 +893,15 @@ enum COLORS {
 
 #endif  /* __COLORS */
 
-#define ANSI_NORMAL     0x100
+#define FG_UNKNOWN	0x100
 #define BG_BLACK        0x200
 #define BG_BRIGHT       0x400       // Not an IBM-CGA/ANSI.SYS compatible attribute
+#define REVERSED        0x800
+#define UNDERLINE	0x1000
+#define CONCEALED	0x2000
+#define BG_UNKNOWN	0x4000
+// TODO: Do we need to keep this value compatible?
+#define ANSI_NORMAL     (FG_UNKNOWN | BG_UNKNOWN)
 #define BG_BLUE         (BLUE << 4)
 #define BG_GREEN        (GREEN << 4)
 #define BG_CYAN         (CYAN << 4)
diff --git a/src/sbbs3/scandirs.cpp b/src/sbbs3/scandirs.cpp
index 0e69e8f902aa177d116a76a96735cce5b003b220..24c2b2cd77c3181f60f958d22deb6a8e07175d32 100644
--- a/src/sbbs3/scandirs.cpp
+++ b/src/sbbs3/scandirs.cpp
@@ -47,7 +47,7 @@ void sbbs_t::scandirs(int mode)
 	SAFEPRINTF2(keys, "%s%c\r", text[DirLibKeys], all_key());
 	ch = (char)getkeys(keys, 0);
 	if (sys_status & SS_ABORT || ch == CR) {
-		lncntr = 0;
+		term->lncntr = 0;
 		return;
 	}
 	if (ch != all_key()) {
@@ -62,12 +62,12 @@ void sbbs_t::scandirs(int mode)
 			if (text[DisplayExtendedFileInfoQ][0] && !noyes(text[DisplayExtendedFileInfoQ]))
 				mode |= FL_EXT;
 			if (sys_status & SS_ABORT) {
-				lncntr = 0;
+				term->lncntr = 0;
 				return;
 			}
 			bputs(text[SearchStringPrompt]);
 			if (!getstr(str, 40, K_LINE | K_UPPER)) {
-				lncntr = 0;
+				term->lncntr = 0;
 				return;
 			}
 		}
@@ -140,12 +140,12 @@ void sbbs_t::scanalldirs(int mode)
 		if (text[DisplayExtendedFileInfoQ][0] && !noyes(text[DisplayExtendedFileInfoQ]))
 			mode |= FL_EXT;
 		if (sys_status & SS_ABORT) {
-			lncntr = 0;
+			term->lncntr = 0;
 			return;
 		}
 		bputs(text[SearchStringPrompt]);
 		if (!getstr(str, 40, K_LINE | K_UPPER)) {
-			lncntr = 0;
+			term->lncntr = 0;
 			return;
 		}
 	}
diff --git a/src/sbbs3/scansubs.cpp b/src/sbbs3/scansubs.cpp
index 3746876070de67359c11e01465b251a00b81a1c9..0d50fd4a27648b316d3220302fd49fb7aba95d56 100644
--- a/src/sbbs3/scansubs.cpp
+++ b/src/sbbs3/scansubs.cpp
@@ -122,7 +122,7 @@ void sbbs_t::scansubs(int mode)
 		}
 		if (mode & SCAN_POLLS) {
 			progress(text[Done], subs_scanned, usrsubs[curgrp]);
-			cleartoeol();
+			term->cleartoeol();
 		}
 		bputs(text[MessageScan]);
 		if (i == usrsubs[curgrp])
@@ -229,7 +229,7 @@ void sbbs_t::scanallsubs(int mode)
 	free(sub);
 	if (mode & SCAN_POLLS) {
 		progress(text[Done], subs_scanned, total_subs);
-		cleartoeol();
+		term->cleartoeol();
 	}
 	bputs(text[MessageScan]);
 	if (subs_scanned < total_subs) {
@@ -336,7 +336,7 @@ void sbbs_t::new_scan_ptr_cfg()
 			snprintf(keys, sizeof keys, "%c%c", all_key(), quit_key());
 			s = getkeys(keys, usrsubs[i]);
 			if (sys_status & SS_ABORT) {
-				lncntr = 0;
+				term->lncntr = 0;
 				return;
 			}
 			if (s == -1 || !s || s == quit_key())
@@ -460,7 +460,7 @@ void sbbs_t::new_scan_cfg(uint misc)
 			snprintf(keys, sizeof keys, "%c%c", all_key(), quit_key());
 			s = getkeys(keys, usrsubs[i]);
 			if (sys_status & SS_ABORT) {
-				lncntr = 0;
+				term->lncntr = 0;
 				return;
 			}
 			if (!s || s == -1 || s == quit_key())
diff --git a/src/sbbs3/str.cpp b/src/sbbs3/str.cpp
index 6f540aadefa5881f4ceee31a01b975f2a272a9de..bb445d6d332acc3d4d2a5b6ca6f730b535f3d555 100644
--- a/src/sbbs3/str.cpp
+++ b/src/sbbs3/str.cpp
@@ -119,7 +119,7 @@ bool sbbs_t::load_user_text(void)
 	char path[MAX_PATH + 1];
 	char charset[16];
 
-	SAFECOPY(charset, term_charset());
+	SAFECOPY(charset, term->charset_str());
 	strlwr(charset);
 	revert_text();
 	snprintf(path, sizeof path, "%s%s/text.ini", cfg.ctrl_dir, charset);
@@ -340,7 +340,7 @@ void sbbs_t::sif(char *fname, char *answers, int len)
 				m++;
 			}
 			if ((buf[m + 1] & 0xdf) == 'L') {      /* Draw line */
-				if (term_supports(COLOR))
+				if (term->supports(COLOR))
 					attr(cfg.color[clr_inputline]);
 				else
 					attr(BLACK | BG_LIGHTGRAY);
@@ -508,7 +508,7 @@ void sbbs_t::sof(char *fname, char *answers, int len)
 			else if ((buf[m + 1] & 0xdf) == 'N')   /* Numbers only */
 				m++;
 			if ((buf[m + 1] & 0xdf) == 'L') {      /* Draw line */
-				if (term_supports(COLOR))
+				if (term->supports(COLOR))
 					attr(cfg.color[clr_inputline]);
 				else
 					attr(BLACK | BG_LIGHTGRAY);
@@ -533,7 +533,7 @@ void sbbs_t::sof(char *fname, char *answers, int len)
 			else if ((buf[m + 1] & 0xdf) == 'N')   /* Numbers only */
 				m++;
 			if ((buf[m + 1] & 0xdf) == 'L') {
-				if (term_supports(COLOR))
+				if (term->supports(COLOR))
 					attr(cfg.color[clr_inputline]);
 				else
 					attr(BLACK | BG_LIGHTGRAY);
@@ -646,22 +646,22 @@ size_t sbbs_t::gettmplt(char *strout, const char *templt, int mode)
 	sys_status &= ~SS_ABORT;
 	SAFECOPY(tmplt, templt);
 	strupr(tmplt);
-	if (term_supports(ANSI)) {
-		if (mode & K_LINE) {
-			if (term_supports(COLOR))
-				attr(cfg.color[clr_inputline]);
-			else
-				attr(BLACK | BG_LIGHTGRAY);
-		}
-		while (c < t) {
-			if (tmplt[c] == 'N' || tmplt[c] == 'A' || tmplt[c] == '!')
-				outchar(' ');
-			else
-				outchar(tmplt[c]);
-			c++;
-		}
-		cursor_left(t);
-	}
+	// MODE7: This was ANSI-only, added support for PETSCII, 
+	//        but we may not want it for Mode7.
+	if (mode & K_LINE) {
+		if (term->supports(COLOR))
+			attr(cfg.color[clr_inputline]);
+		else
+			attr(BLACK | BG_LIGHTGRAY);
+	}
+	while (c < t) {
+		if (tmplt[c] == 'N' || tmplt[c] == 'A' || tmplt[c] == '!')
+			outchar(' ');
+		else
+			outchar(tmplt[c]);
+		c++;
+	}
+	term->cursor_left(t);
 	c = 0;
 	if (mode & K_EDIT) {
 		SAFECOPY(str, strout);
@@ -675,7 +675,7 @@ size_t sbbs_t::gettmplt(char *strout, const char *templt, int mode)
 			for (ch = 1, c--; c; c--, ch++)
 				if (tmplt[c] == 'N' || tmplt[c] == 'A' || tmplt[c] == '!')
 					break;
-			cursor_left(ch);
+			term->cursor_left(ch);
 			bputs(" \b");
 			continue;
 		}
@@ -1120,7 +1120,7 @@ char* sbbs_t::xfer_prot_menu(enum XFER_TYPE type, user_t* user, char* keys, size
 	bool   menu_used = menu(prot_menu_file[type], P_NOERROR);
 	if (user == nullptr)
 		user = &useron;
-	cond_blankline();
+	term->cond_blankline();
 	int    printed = 0;
 	for (int i = 0; i < cfg.total_prots; i++) {
 		if (!chk_ar(cfg.prot[i]->ar, user, &client))
@@ -1137,7 +1137,7 @@ char* sbbs_t::xfer_prot_menu(enum XFER_TYPE type, user_t* user, char* keys, size
 			keys[count++] = cfg.prot[i]->mnemonic;
 		if (menu_used)
 			continue;
-		if (printed && (cols < 80 || (printed % 2) == 0))
+		if (printed && (term->cols < 80 || (printed % 2) == 0))
 			CRLF;
 		bprintf(text[TransferProtLstFmt], cfg.prot[i]->mnemonic, cfg.prot[i]->name);
 		printed++;
@@ -1145,7 +1145,7 @@ char* sbbs_t::xfer_prot_menu(enum XFER_TYPE type, user_t* user, char* keys, size
 	if (keys != nullptr)
 		keys[count] = '\0';
 	if (!menu_used)
-		newline();
+		term->newline();
 	return keys;
 }
 
@@ -1298,7 +1298,7 @@ bool sbbs_t::spy(uint i /* node_num */)
 			continue;
 		}
 		if (ch < ' ') {
-			lncntr = 0;                       /* defeat pause */
+			term->lncntr = 0;                       /* defeat pause */
 			spy_socket[i - 1] = INVALID_SOCKET; /* disable spy output */
 			ch = handle_ctrlkey(ch, K_NONE);
 			spy_socket[i - 1] = passthru_thread_running ? client_socket_dup : client_socket;  /* enable spy output */
diff --git a/src/sbbs3/telgate.cpp b/src/sbbs3/telgate.cpp
index 2d3481b7d042c7764c200679f1068cabadd94db3..9671e5c336f3a52b7ef625386175eb02c7742eb2 100644
--- a/src/sbbs3/telgate.cpp
+++ b/src/sbbs3/telgate.cpp
@@ -173,14 +173,14 @@ struct TelnetProxy
 							buf[0] = TELNET_IAC;
 							buf[1] = TELNET_SB;
 							buf[2] = TELNET_NEGOTIATE_WINDOW_SIZE;
-							buf[3] = (sbbs->cols >> 8) & 0xff;
-							buf[4] = sbbs->cols & 0xff;
-							buf[5] = (sbbs->rows >> 8) & 0xff;
-							buf[6] = sbbs->rows & 0xff;
+							buf[3] = (sbbs->term->cols >> 8) & 0xff;
+							buf[4] = sbbs->term->cols & 0xff;
+							buf[5] = (sbbs->term->rows >> 8) & 0xff;
+							buf[6] = sbbs->term->rows & 0xff;
 							buf[7] = TELNET_IAC;
 							buf[8] = TELNET_SE;
 							if (sbbs->startup->options & BBS_OPT_DEBUG_TELNET)
-								sbbs->lprintf(LOG_DEBUG, "%s: Window Size is %u x %u", __FUNCTION__, sbbs->cols, sbbs->rows);
+								sbbs->lprintf(LOG_DEBUG, "%s: Window Size is %u x %u", __FUNCTION__, sbbs->term->cols, sbbs->term->rows);
 							if (::sendsocket(sock, (char *)buf, 9) != 9)
 								lprintf(LOG_WARNING, "%s: Failed to send Window Size command", __FUNCTION__);
 						}
diff --git a/src/sbbs3/terminal.cpp b/src/sbbs3/terminal.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..3f24e6c5f2e253437d84acc117378b8c3fd06a0c
--- /dev/null
+++ b/src/sbbs3/terminal.cpp
@@ -0,0 +1,467 @@
+#include "terminal.h"
+#include "ansi_terminal.h"
+#include "petscii_term.h"
+#include "link_list.h"
+
+void Terminal::clear_hotspots(void)
+{
+	if (!(flags_ & MOUSE))
+		return;
+	int spots = listCountNodes(mouse_hotspots);
+	if (spots) {
+#if 0 //def _DEBUG
+		sbbs->lprintf(LOG_DEBUG, "Clearing %ld mouse hot spots", spots);
+#endif
+		listFreeNodes(mouse_hotspots);
+		if (!(sbbs->console & CON_MOUSE_SCROLL))
+			set_mouse(MOUSE_MODE_OFF);
+	}
+}
+
+void Terminal::scroll_hotspots(unsigned count)
+{
+	if (!(flags_ & MOUSE))
+		return;
+	unsigned spots = 0;
+	unsigned remain = 0;
+	for (list_node_t* node = mouse_hotspots->first; node != NULL; node = node->next) {
+		struct mouse_hotspot* spot = (struct mouse_hotspot*)node->data;
+		spot->y -= count;
+		spots++;
+		if (spot->y >= 0)
+			remain++;
+	}
+#ifdef _DEBUG
+	if (spots)
+		sbbs->lprintf(LOG_DEBUG, "Scrolled %u mouse hot-spots %u rows (%u remain)", spots, count, remain);
+#endif
+	if (remain < 1)
+		clear_hotspots();
+}
+
+struct mouse_hotspot* Terminal::add_hotspot(struct mouse_hotspot* spot)
+{
+	if (!(sbbs->cfg.sys_misc & SM_MOUSE_HOT) || !supports(MOUSE))
+		return nullptr;
+	if (spot->y == HOTSPOT_CURRENT_Y)
+		spot->y = row;
+	if (spot->minx == HOTSPOT_CURRENT_X)
+		spot->minx = column;
+	if (spot->maxx == HOTSPOT_CURRENT_X)
+		spot->maxx = cols - 1;
+#if 0 //def _DEBUG
+	char         dbg[128];
+	sbbs->lprintf(LOG_DEBUG, "Adding mouse hot spot %ld-%ld x %ld = '%s'"
+		, spot->minx, spot->maxx, spot->y, c_escape_str(spot->cmd, dbg, sizeof(dbg), /* Ctrl-only? */ true));
+#endif
+	list_node_t* node = listInsertNodeData(mouse_hotspots, spot, sizeof(*spot));
+	if (node == nullptr)
+		return nullptr;
+	set_mouse(MOUSE_MODE_ON);
+	return (struct mouse_hotspot*)node->data;
+}
+
+struct mouse_hotspot* Terminal::add_hotspot(char cmd, bool hungry, unsigned minx, unsigned maxx, unsigned y)
+{
+	if (!(flags_ & MOUSE))
+		return nullptr;
+	struct mouse_hotspot spot = {};
+	spot.cmd[0] = cmd;
+	spot.minx = minx;
+	spot.maxx = maxx;
+	spot.y = y;
+	spot.hungry = hungry;
+	return add_hotspot(&spot);
+}
+
+bool Terminal::add_pause_hotspot(char cmd)
+{
+	if (!(flags_ & MOUSE))
+		return false;
+	if (mouse_hotspots->first != nullptr)
+		return false;
+	struct mouse_hotspot spot = {};
+	spot.cmd[0] = cmd;
+	spot.minx = column;
+	spot.maxx = column;
+	spot.y = HOTSPOT_CURRENT_Y;
+	spot.hungry = true;
+	if (add_hotspot(&spot) != nullptr)
+		return true;
+	return false;
+}
+
+struct mouse_hotspot* Terminal::add_hotspot(int num, bool hungry, unsigned minx, unsigned maxx, unsigned y)
+{
+	if (!(flags_ & MOUSE))
+		return nullptr;
+	struct mouse_hotspot spot = {};
+	SAFEPRINTF(spot.cmd, "%d\r", num);
+	spot.minx = minx;
+	spot.maxx = maxx;
+	spot.y = y;
+	spot.hungry = hungry;
+	return add_hotspot(&spot);
+}
+
+struct mouse_hotspot* Terminal::add_hotspot(uint num, bool hungry, unsigned minx, unsigned maxx, unsigned y)
+{
+	if (!(flags_ & MOUSE))
+		return nullptr;
+	struct mouse_hotspot spot = {};
+	SAFEPRINTF(spot.cmd, "%u\r", num);
+	spot.minx = minx;
+	spot.maxx = maxx;
+	spot.y = y;
+	spot.hungry = hungry;
+	return add_hotspot(&spot);
+}
+
+struct mouse_hotspot* Terminal::add_hotspot(const char* cmd, bool hungry, unsigned minx, unsigned maxx, unsigned y)
+{
+	if (!(flags_ & MOUSE))
+		return nullptr;
+	struct mouse_hotspot spot = {};
+	SAFECOPY(spot.cmd, cmd);
+	spot.minx = minx;
+	spot.maxx = maxx;
+	spot.y = y;
+	spot.hungry = hungry;
+	return add_hotspot(&spot);
+}
+
+void Terminal::inc_row(unsigned count) {
+	row += count;
+	if (row >= rows) {
+		scroll_hotspots((row - rows) + 1);
+		row = rows - 1;
+	}
+	if (lncntr || lastcrcol)
+		lncntr += count;
+	if (!suspend_lbuf)
+		lbuflen = 0;
+}
+
+// TODO: ANSI does *not* specify what happens at the end of a line, and
+//       there are at least three common behaviours (in decresing order
+//       of popularity):
+//       1) Do the DEC Last Column Flag thing where it hangs out in the
+//          last column until a printing character is received, then
+//          decides if it should wrap or not
+//       2) Wrap immediately when printed to the last column
+//       3) Stay in the last column and replace the character
+//
+//       We assume that #2 happens here, but most terminals do #1... which
+//       usually looks like #2, but is slightly different.  Generally, any
+//       terminal that does #1 can be switched to do #3, and most terminals
+//       that do #2 can also do #3.
+//
+//       It's fairly simple in ANSI to detect which happens, but is not
+//       possible for dumb terminals (though I don't think I've ever seen
+//       anything but #2 there).  For things like VT52, PETSCII, and Mode7,
+//       the behaviour is well established.
+//
+//       We can easily emulate #2 if we know if #1 or #3 is currently
+//       occuring.  It's possible to emulate #1 with #2 as long as
+//       insert character is available, though it's trickier.
+//
+//       The problem with emulating #2 is that it scrolls the screen when
+//       the bottom-right cell is written to, which can have undesired
+//       consequences.  It also requires the suppression of CRLF after
+//       column 80 is written, which breaks the "a line is a line"
+//       abstraction.
+//
+//       The best would be to switch to #3 when possible, and emulate #1
+//       The behaviour would then match the most common implementations
+//       and be well controlled.  If only #1 is available, we can still
+//       always work as expected (modulo some variations between exact
+//       flag clearing conditions), and be able to avoid the scroll.
+//       Only terminals that implement #2 (rare) and don't allow switching
+//       to #3 (I'm not aware of any) would have problems.
+//
+//       This would resolve the long-standing issue of printing to column
+//       80 which almost every sysop that customizes their BBS has ran
+//       across in the past.
+//
+//       The sticky wicket here is what to do about doors.  The vast
+//       majority of them assume #2, but are tested with #1 they also
+//       generally assume 80x24.  It would be interesting to have a mode
+//       that (optionally) centres the door output in the current
+//       terminal.
+void Terminal::inc_column(unsigned count) {
+	column += count;
+	if (column >= cols)
+		lastcrcol = cols;
+	while (column >= cols) {
+		if (!suspend_lbuf)
+			lbuflen = 0;
+		column -= cols;
+		inc_row();
+	}
+}
+
+void Terminal::dec_row(unsigned count) {
+	// Never allow dec_row to scroll up
+	if (count > row)
+		count = row;
+#if 0
+	// NOTE: If we do allow scrolling up, scroll_hotspots needs to get signed
+	if (count > row) {
+		scroll_hotspots(row - count);
+	}
+#endif
+	row -= count;
+	if (count > lncntr)
+		count = lncntr;
+	lncntr -= count;
+	if (!suspend_lbuf)
+		lbuflen = 0;
+}
+
+void Terminal::dec_column(unsigned count) {
+	// Never allow dec_column() to wrap
+	if (count > column)
+		count = column;
+	column -= count;
+	if (column == 0) {
+		if (!suspend_lbuf)
+			lbuflen = 0;
+	}
+}
+
+void Terminal::set_row(unsigned val) {
+	if (val >= rows)
+		val = rows - 1;
+	row = val;
+	lncntr = 0;
+	if (!suspend_lbuf)
+		lbuflen = 0;
+}
+
+void Terminal::set_column(unsigned val) {
+	if (val >= cols)
+		val = cols - 1;
+	column = val;
+}
+
+void Terminal::cond_newline() {
+	if (column > 0)
+		newline();
+}
+
+void Terminal::cond_blankline() {
+	cond_newline();
+	if (lastcrcol)
+		newline();
+}
+
+void Terminal::cond_contline() {
+	if (column > 0 && cols < TERM_COLS_DEFAULT)
+		sbbs->bputs(sbbs->text[LongLineContinuationPrefix]);
+}
+
+bool Terminal::supports(unsigned cmp_flags) {
+	return (flags_ & cmp_flags) == cmp_flags;
+}
+
+bool Terminal::supports_any(unsigned cmp_flags) {
+	return (flags_ & cmp_flags);
+}
+
+uint32_t Terminal::charset() {
+	return (flags_ & CHARSET_FLAGS);
+}
+
+const char * Terminal::charset_str()
+{
+	switch(charset()) {
+		case CHARSET_PETSCII:
+			return "CBM-ASCII";
+		case CHARSET_UTF8:
+			return "UTF-8";
+		case CHARSET_ASCII:
+			return "US-ASCII";
+		default:
+			return "CP437";
+	}
+}
+
+list_node_t *Terminal::find_hotspot(unsigned x, unsigned y)
+{
+	list_node_t *node;
+
+	for (node = mouse_hotspots->first; node != nullptr; node = node->next) {
+		struct mouse_hotspot* spot = (struct mouse_hotspot*)node->data;
+		if (spot->y == y && x >= spot->minx && x <= spot->maxx)
+			break;
+	}
+	if (node == nullptr) {
+		for (node = mouse_hotspots->first; node != nullptr; node = node->next) {
+			struct mouse_hotspot* spot = (struct mouse_hotspot*)node->data;
+			if (spot->hungry && spot->y == y && x >= spot->minx)
+				break;
+		}
+	}
+	if (node == NULL) {
+		for (node = mouse_hotspots->last; node != nullptr; node = node->prev) {
+			struct mouse_hotspot* spot = (struct mouse_hotspot*)node->data;
+			if (spot->hungry && spot->y == y && x <= spot->minx)
+				break;
+		}
+	}
+
+	return node;
+}
+
+uint32_t Terminal::flags(bool raw)
+{
+	if (!raw) {
+		uint32_t newflags = get_flags(sbbs);
+		if (newflags != flags_)
+			update_terminal(sbbs);
+	}
+	// We have potentially destructed ourselves now...
+	return sbbs->term->flags_;
+}
+
+void Terminal::insert_indicator() {
+	// Defeat line buffer
+	suspend_lbuf = true;
+	if (save_cursor_pos() && gotoxy(cols, 1)) {
+		const unsigned orig_atr{sbbs->curatr};
+		if (sbbs->console & CON_INSERT) {
+			sbbs->attr(BLINK | BLACK | (LIGHTGRAY << 4));
+			sbbs->cp437_out('I');
+		} else {
+			sbbs->attr(LIGHTGRAY);
+			sbbs->cp437_out(' ');
+		}
+		restore_cursor_pos();
+		sbbs->attr(orig_atr);
+	}
+	suspend_lbuf = false;
+}
+
+char *Terminal::attrstr(unsigned newattr, char *str, size_t strsz)
+{
+	return attrstr(newattr, curatr, str, strsz);
+}
+
+/*
+ * Increments columns appropriately for UTF-8 codepoint
+ * Parses the codepoint from successive uchars
+ */
+bool Terminal::utf8_increment(unsigned char ch)
+{
+	if (flags_ & UTF8) {
+		// TODO: How many errors should be logged?
+		if (utf8_remain > 0) {
+			// First, check if this is overlong...
+			if (first_continuation
+			    && ((utf8_remain == 1 && ch < 0xA0)
+			    || (utf8_remain == 2 && ch < 0x90)
+			    || (utf8_remain == 3 && ch >= 0x90))) {
+				sbbs->lprintf(LOG_WARNING, "Sending invalid UTF-8 codepoint");
+				first_continuation = false;
+				utf8_remain = 0;
+			}
+			else if ((ch & 0xc0) != 0x80) {
+				first_continuation = false;
+				utf8_remain = 0;
+				sbbs->lprintf(LOG_WARNING, "Sending invalid UTF-8 codepoint");
+			}
+			else {
+				first_continuation = false;
+				utf8_remain--;
+				codepoint <<= 6;
+				codepoint |= (ch & 0x3f);
+				if (utf8_remain)
+					return true;
+				inc_column(unicode_width(static_cast<enum unicode_codepoint>(codepoint), 0));
+				codepoint = 0;
+				return true;
+			}
+		}
+		else if ((ch & 0x80) != 0) {
+			if ((ch == 0xc0) || (ch == 0xc1) || (ch > 0xF5)) {
+				sbbs->lprintf(LOG_WARNING, "Sending invalid UTF-8 codepoint");
+			}
+			if ((ch & 0xe0) == 0xc0) {
+				utf8_remain = 1;
+				if (ch == 0xE0)
+					first_continuation = true;
+				codepoint = ch & 0x1F;
+				return true;
+			}
+			else if ((ch & 0xf0) == 0xe0) {
+				utf8_remain = 2;
+				if (ch == 0xF0)
+					first_continuation = true;
+				codepoint = ch & 0x0F;
+				return true;
+			}
+			else if ((ch & 0xf8) == 0xf0) {
+				utf8_remain = 3;
+				if (ch == 0xF4)
+					first_continuation = true;
+				codepoint = ch & 0x07;
+				return true;
+			}
+			else
+				sbbs->lprintf(LOG_WARNING, "Sending invalid UTF-8 codepoint");
+		}
+		if (utf8_remain)
+			return true;
+	}
+	return false;
+}
+
+void update_terminal(sbbs_t *sbbsptr)
+{
+	uint32_t flags = Terminal::get_flags(sbbsptr);
+	if (sbbsptr->term == nullptr) {
+		if (flags & PETSCII)
+			sbbsptr->term = new PETSCII_Terminal(sbbsptr);
+		else if (flags & (ANSI))
+			sbbsptr->term = new ANSI_Terminal(sbbsptr);
+		else
+			sbbsptr->term = new Terminal(sbbsptr);
+	}
+	else {
+		Terminal *newTerm;
+		if (flags & PETSCII)
+			newTerm = new PETSCII_Terminal(sbbsptr->term);
+		else if (flags & (ANSI))
+			newTerm = new ANSI_Terminal(sbbsptr->term);
+		else
+			newTerm = new Terminal(sbbsptr->term);
+		delete sbbsptr->term;
+		sbbsptr->term = newTerm;
+	}
+	sbbsptr->term->updated();
+}
+
+void update_terminal(sbbs_t *sbbsptr, Terminal *term)
+{
+	uint32_t flags = term->flags(true);
+	Terminal *newTerm;
+	if (flags & PETSCII)
+		newTerm = new PETSCII_Terminal(sbbsptr, term);
+	else if (flags & (ANSI))
+		newTerm = new ANSI_Terminal(sbbsptr, term);
+	else
+		newTerm = new Terminal(sbbsptr, term);
+	if (sbbsptr->term)
+		delete sbbsptr->term;
+	sbbsptr->term = newTerm;
+	sbbsptr->term->updated();
+}
+
+extern "C" void update_terminal(void *sbbsptr, user_t *userptr)
+{
+	if (sbbsptr == nullptr || userptr == nullptr)
+		return;
+	sbbs_t *sbbs = static_cast<sbbs_t *>(sbbsptr);
+	if (sbbs->useron.number == userptr->number)
+		update_terminal(sbbs);
+}
diff --git a/src/sbbs3/terminal.h b/src/sbbs3/terminal.h
new file mode 100644
index 0000000000000000000000000000000000000000..69805ed302878a8d3f1760134fc52125fee1bb76
--- /dev/null
+++ b/src/sbbs3/terminal.h
@@ -0,0 +1,538 @@
+#ifndef TERMINAL_H
+#define TERMINAL_H
+
+#include "sbbs.h"
+#include "utf8.h"
+#include "unicode.h"
+
+#ifdef __cplusplus
+
+struct mouse_hotspot {          // Mouse hot-spot
+	char     cmd[128];
+	unsigned y;
+	unsigned minx;
+	unsigned maxx;
+	bool     hungry;
+};
+
+struct savedline {
+	char buf[LINE_BUFSIZE + 1];     /* Line buffer (i.e. ANSI-encoded) */
+	uint beg_attr;                  /* Starting attribute of each line */
+	uint end_attr;                  /* Ending attribute of each line */
+	int column;                     /* Current column number */
+};
+
+enum output_rate {
+	output_rate_unlimited,
+	output_rate_300 = 300,
+	output_rate_600 = 600,
+	output_rate_1200 = 1200,
+	output_rate_2400 = 2400,
+	output_rate_4800 = 4800,
+	output_rate_9600 = 9600,
+	output_rate_19200 = 19200,
+	output_rate_38400 = 38400,
+	output_rate_57600 = 57600,
+	output_rate_76800 = 76800,
+	output_rate_115200 = 115200,
+};
+
+// Terminal mouse reporting mode (mouse_mode)
+#define MOUSE_MODE_OFF  0       // No terminal mouse reporting enabled/expected
+#define MOUSE_MODE_X10  (1<<0)  // X10 compatible mouse reporting enabled
+#define MOUSE_MODE_NORM (1<<1)  // Normal tracking mode mouse reporting
+#define MOUSE_MODE_BTN  (1<<2)  // Button-event tracking mode mouse reporting
+#define MOUSE_MODE_ANY  (1<<3)  // Any-event tracking mode mouse reporting
+#define MOUSE_MODE_EXT  (1<<4)  // SGR-encoded extended coordinate mouse reporting
+#define MOUSE_MODE_ON   (MOUSE_MODE_NORM | MOUSE_MODE_EXT) // Default mouse "enabled" mode flags
+
+#define HOTSPOT_CURRENT_X UINT_MAX
+#define HOTSPOT_CURRENT_Y UINT_MAX
+
+class Terminal {
+public:
+	unsigned row{0};                   /* Current row */
+	unsigned column{80};               /* Current column counter (for line counter) */
+	unsigned rows{24};                 /* Current number of Rows for User */
+	unsigned cols{0};                  /* Current number of Columns for User */
+	unsigned tabstop{8};               /* Current symmetric-tabstop (size) */
+	unsigned lastcrcol{0};             /* Column when last CR occured (previously lastlinelen) */
+	unsigned cterm_version{0};	   /* (MajorVer*1000) + MinorVer */
+	unsigned lncntr{0};                /* Line Counter - for PAUSE */
+	unsigned latr{ANSI_NORMAL};        /* Starting attribute of line buffer */
+	uint32_t curatr{ANSI_NORMAL};      /* Current Text Attributes Always */
+	unsigned lbuflen{0};               /* Number of characters in line buffer */
+	char     lbuf[LINE_BUFSIZE + 1]{}; /* Temp storage for each line output */
+	enum output_rate cur_output_rate{output_rate_unlimited};
+	unsigned mouse_mode{MOUSE_MODE_OFF};            // Mouse reporting mode flags
+	bool pause_hotspot{false};
+	bool suspend_lbuf{0};
+	link_list_t *mouse_hotspots{nullptr};
+
+protected:
+	sbbs_t* sbbs;
+	uint32_t flags_{0};                 /* user.misc flags that impact the terminal */
+
+private:
+	link_list_t *savedlines{nullptr};
+	uint8_t utf8_remain{0};
+	bool first_continuation{false};
+	uint32_t codepoint{0};
+
+public:
+
+	static uint32_t flags_fixup(uint32_t flags)
+	{
+		if (flags & UTF8) {
+			// These bits are *never* available in UTF8 mode
+			// Note that RIP is not inherently incompatible with UTF8
+			flags &= ~(NO_EXASCII | PETSCII);
+		}
+
+		if (flags & RIP) {
+			// ANSI is always available when RIP is
+			flags |= ANSI;
+		}
+
+		if (!(flags & ANSI)) {
+			// These bits are *only* available in ANSI mode
+			// NOTE: COLOR is forced in PETSCII mode later
+			flags &= ~(COLOR | RIP | ICE_COLOR | MOUSE);
+		}
+		else {
+			// These bits are *never* available in ANSI mode
+			flags &= ~(PETSCII);
+		}
+
+		if (flags & PETSCII) {
+			// These bits are *never* available in PETSCII mode
+			flags &= ~(RIP | ICE_COLOR | MOUSE | NO_EXASCII | UTF8);
+			// These bits are *always* availabe in PETSCII mode
+			flags |= COLOR;
+		}
+		return flags;
+	}
+
+	static uint32_t get_flags(sbbs_t *sbbsptr) {
+		uint32_t flags;
+
+		if (sbbsptr->sys_status & (SS_USERON | SS_NEWUSER)) {
+			if (sbbsptr->useron.misc & AUTOTERM) {
+				flags = sbbsptr->autoterm;
+				flags |= sbbsptr->useron.misc & (NO_EXASCII | SWAP_DELETE | COLOR | ICE_COLOR | MOUSE);
+			}
+			else
+				flags = sbbsptr->useron.misc;
+		}
+		else
+			flags = sbbsptr->autoterm;
+		flags &= TERM_FLAGS;
+		// TODO: Get rows and cols
+		return flags_fixup(flags);
+	}
+
+	static link_list_t *listPtrInit(int flags) {
+		link_list_t *ret = new link_list_t;
+		listInit(ret, flags);
+		return ret;
+	}
+
+	static void listPtrFree(link_list_t *ll) {
+		listFree(ll);
+		delete ll;
+	}
+
+	Terminal() = delete;
+	// Create from sbbs_t*, ie: "Create new"
+	Terminal(sbbs_t *sbbsptr) : mouse_hotspots{listPtrInit(0)}, sbbs{sbbsptr},
+	    flags_{get_flags(sbbsptr)}, savedlines{listPtrInit(0)} {}
+
+	// Create from Terminal*, ie: Update
+	Terminal(Terminal *t) : row{t->row}, column{t->column},
+	    rows{t->rows}, cols{t->cols}, tabstop{t->tabstop}, lastcrcol{t->lastcrcol}, 
+	    cterm_version{t->cterm_version}, lncntr{t->lncntr}, latr{t->latr}, curatr{t->curatr},
+	    lbuflen{t->lbuflen}, mouse_mode{t->mouse_mode}, pause_hotspot{t->pause_hotspot},
+	    suspend_lbuf{t->suspend_lbuf},
+	    mouse_hotspots{t->mouse_hotspots}, sbbs{t->sbbs}, flags_{get_flags(t->sbbs)},
+	    savedlines{t->savedlines} {
+		// Take ownership of lists so they're not destroyed
+		t->mouse_hotspots = nullptr;
+		t->savedlines = nullptr;
+		// Copy line buffer
+		memcpy(lbuf, t->lbuf, sizeof(lbuf));
+		// TODO: This is pretty hacky...
+		//       Calls the old set_mouse() with the current flags
+		//       and mode.  We can't call the new one because it's
+		//       virtual and we aren't constructed yet.
+		//       Ideally this would disable the mouse if !supports(MOUSE)
+		t->flags_ = flags_;
+		t->set_mouse(mouse_mode);
+	}
+
+	// Create from sbbsptr* and Terminal*, ie: Create a copy
+	Terminal(sbbs_t *sbbsptr, Terminal *t) : row{t->row}, column{t->column},
+	    rows{t->rows}, cols{t->cols}, tabstop{t->tabstop}, lastcrcol{t->lastcrcol}, 
+	    cterm_version{t->cterm_version}, lncntr{t->lncntr}, latr{t->latr}, curatr{t->curatr},
+	    lbuflen{t->lbuflen}, mouse_mode{t->mouse_mode}, pause_hotspot{t->pause_hotspot},
+	    suspend_lbuf{t->suspend_lbuf},
+	    mouse_hotspots{listPtrInit(0)}, sbbs{sbbsptr}, flags_{get_flags(t->sbbs)},
+	    savedlines{listPtrInit(0)} {}
+
+	virtual ~Terminal()
+	{
+		listPtrFree(mouse_hotspots);
+		listPtrFree(savedlines);
+	}
+
+	// Was ansi()
+	virtual const char *attrstr(unsigned atr) {
+		return "";
+	}
+
+	// Was ansi() and ansi_attr()
+	virtual char* attrstr(unsigned atr, unsigned curatr, char* str, size_t strsz) {
+		if (strsz > 0)
+			str[0] = 0;
+		return str;
+	}
+
+	virtual bool getdims() {
+		return false;
+	}
+
+	virtual bool getxy(unsigned* x, unsigned* y) {
+		if (x)
+			*x = column + 1;
+		if (y)
+			*y = row + 1;
+		return true;
+	}
+
+	virtual bool gotoxy(unsigned x, unsigned y) {
+		return false;
+	}
+
+	// Was ansi_save
+	virtual bool save_cursor_pos() {
+		return false;
+	}
+
+	// Was ansi_restore
+	virtual bool restore_cursor_pos() {
+		return false;
+	}
+
+	virtual void carriage_return() {
+		sbbs->term_out('\r');
+	}
+
+	virtual void line_feed(unsigned count = 1) {
+		for (unsigned i = 0; i < count; i++)
+			sbbs->term_out('\n');
+	}
+
+	/*
+	 * Destructive backspace.
+	 */
+	virtual void backspace(unsigned int count = 1) {
+		for (unsigned i = 0; i < count; i++) {
+			if (column > 0)
+				sbbs->term_out("\b \b");
+			else
+				break;
+		}
+	}
+
+	virtual void newline(unsigned count = 1) {
+		// TODO: Original version did not increment row or lncntr
+		//       It recursed through outchar()
+		for (unsigned i = 0; i < count; i++) {
+			carriage_return();
+			line_feed();
+			sbbs->check_pause();
+		}
+	}
+
+	virtual void clearscreen() {
+		clear_hotspots();
+		sbbs->term_out(FF);
+		lastcrcol = 0;
+	}
+
+	virtual void cleartoeos() {}
+	virtual void cleartoeol() {}
+	virtual void clearline() {
+		carriage_return();
+		cleartoeol();
+	}
+
+	virtual void cursor_home() {}
+	virtual void cursor_up(unsigned count = 1) {}
+	virtual void cursor_down(unsigned count = 1) {
+		line_feed(count);
+	}
+
+	virtual void cursor_right(unsigned count = 1) {
+		for (unsigned i = 0; i < count; i++) {
+			if (column < (cols - 1))
+				sbbs->term_out(' ');
+			else
+				break;
+		}
+	}
+
+	virtual void cursor_left(unsigned count = 1) {
+		for (unsigned i = 0; i < count; i++) {
+			if (column > 0)
+				sbbs->term_out('\b');
+			else
+				break;
+		}
+	}
+	virtual void set_output_rate(enum output_rate speed) {}
+	virtual void center(const char *instr, bool msg = false, unsigned columns = 0) {
+		if (columns == 0)
+			columns = cols;
+		char *str = strdup(instr);
+		truncsp(str);
+		size_t len = bstrlen(str);
+		carriage_return();
+		if (len < columns)
+			cursor_right((columns - len) / 2);
+		if (msg)
+			sbbs->putmsg(str, P_NONE);
+		else
+			sbbs->bputs(str);
+		free(str);
+		newline();
+	}
+
+	/****************************************************************************/
+	/* Returns the printed columns from 'str' accounting for Ctrl-A codes       */
+	/****************************************************************************/
+	virtual size_t bstrlen(const char *str, int mode = 0)
+	{
+		str = sbbs->auto_utf8(str, mode);
+		size_t      count = 0;
+		const char* end = str + strlen(str);
+		while (str < end) {
+			int len = 1;
+			if (*str == CTRL_A) {
+				str++;
+				if (*str == 0 || *str == 'Z')    // EOF
+					break;
+				if (*str == '[') // CR
+					count = 0;
+				else if (*str == '<' && count) // ND-Backspace
+					count--;
+			} else if (((*str) & 0x80) && (mode & P_UTF8)) {
+				enum unicode_codepoint codepoint = UNICODE_UNDEFINED;
+				len = utf8_getc(str, end - str, &codepoint);
+				if (len < 1)
+					break;
+				count += unicode_width(codepoint, sbbs->unicode_zerowidth);
+			} else
+				count++;
+			str += len;
+		}
+		return count;
+	}
+
+	// TODO: backfill?
+	virtual const char* type() {
+		return "DUMB";
+	}
+
+	virtual void set_mouse(unsigned flags) {}
+
+	/*
+	 * Returns true if the caller should send the char, false if
+	 * this function handled it (ie: via term_out(), or stripping it)
+	 */
+	virtual bool parse_output(char ch) {
+		if (utf8_increment(ch))
+			return true;
+
+		switch (ch) {
+			// Zero-width characters we likely shouldn't send
+			case 0:  // NUL
+			case 1:  // SOH
+			case 2:  // STX
+			case 3:  // ETX
+			case 4:  // EOT
+			case 5:  // ENQ
+			case 6:  // ACK
+			case 11: // VT - May be supported... TODO
+			case 14: // SO (7-bit) or LS1 (8-bit)
+			case 15: // SI (7-bit) or LS0 (8-bit)
+			case 16: // DLE
+			case 17: // DC1 / XON
+			case 18: // DC2
+			case 19: // DC3 / XOFF
+			case 20: // DC4
+			case 21: // NAK
+			case 22: // SYN
+			case 23: // STB
+			case 24: // CAN
+			case 25: // EM
+			case 26: // SUB
+			case 28: // FS
+			case 29: // GS
+			case 30: // RS
+			case 31: // US
+				return false;
+
+			// Zero-width characters we want to pass through
+			case 27: // ESC - This one is especially troubling, but we need to pass it for ANSI detection
+				return true;
+			case 7:  // BEL
+				// Does not go into lbuf...
+				return true;
+
+			// Specials
+			case 8:  // BS
+				if (column)
+					dec_column();
+				return true;
+			case 9:	// TAB
+				// TODO: This makes the position unknown
+				if (column < (cols - 1)) {
+					inc_column();
+					while ((column < (cols - 1)) && (column % 8))
+						inc_column();
+				}
+				return true;
+			case 10: // LF
+				inc_row();
+				return true;
+			case 12: // FF
+				// TODO: This makes the position unknown
+				set_row();
+				set_column();
+				return true;
+			case 13: // CR
+				lastcrcol = column;
+				if (sbbs->console & CON_CR_CLREOL)
+					cleartoeol();
+				set_column();
+				return true;
+
+			// Everything else is assumed one byte wide
+			default:
+				inc_column();
+				return true;
+		}
+		return false;
+	}
+
+	/*
+	 * Returns true if inkey() should return the value of ch
+	 * Returns false if inkey() should parse as a ctrl character.
+	 * 
+	 * If ch is a sequence introducer, this should parse the whole
+	 * sequence.  If the whole sequence can be replaced with a single
+	 * control character, ch should be updated to that character, and
+	 * the function should return true.
+	 * 
+	 * This can add additional characters by using ungetkeys? with
+	 * insert as true.  However, to avoid infinite loops, when the
+	 * first key this translates to is itself a control character,
+	 * ch should be update to that one, and this should return true.
+	 * 
+	 * Will replace ch with a TERM_KEY_* or CTRL_*, value if it
+	 * returns true.
+	 * 
+	 * ch is the control character that was received.
+	 * mode is the mode passed to the inkey() call that received ch
+	 */
+	virtual bool parse_input_sequence(char& ch, int mode) { return false; }
+
+	virtual bool saveline() {
+		struct savedline line;
+		line.beg_attr = latr;
+		line.end_attr = curatr;
+		line.column = column;
+		snprintf(line.buf, sizeof(line.buf), "%.*s", lbuflen, lbuf);
+		lbuflen = 0;
+		return listPushNodeData(savedlines, &line, sizeof(line)) != NULL;
+	}
+
+	virtual bool restoreline() {
+		struct savedline* line = (struct savedline*)listPopNode(savedlines);
+		if (line == NULL)
+			return false;
+		// Moved insert_indicator() to first to avoid messing
+		// up the line buffer with it.
+		// Now behaves differently on line 1 (where we should
+		// never actually see it)
+		insert_indicator();
+		sbbs->attr(line->beg_attr);
+		// Switch from rputs to term_out()
+		// This way we don't need to re-encode
+		lbuflen = 0;
+		sbbs->term_out(line->buf);
+		curatr = line->end_attr;
+		free(line);
+		return true;
+	}
+
+	virtual bool can_highlight() {
+		return false;
+	}
+
+	virtual bool can_move() {
+		return false;
+	}
+
+	virtual bool can_mouse() {
+		return false;
+	}
+
+	virtual bool is_monochrome() {
+		return true;
+	}
+
+	virtual void updated() {}
+
+	void clear_hotspots(void);
+	void scroll_hotspots(unsigned count);
+
+	struct mouse_hotspot* add_hotspot(struct mouse_hotspot* spot);
+	struct mouse_hotspot* add_hotspot(char cmd, bool hungry = true, unsigned minx = HOTSPOT_CURRENT_X, unsigned maxx = HOTSPOT_CURRENT_X, unsigned y = HOTSPOT_CURRENT_Y);
+	struct mouse_hotspot* add_hotspot(int num, bool hungry = true, unsigned minx = HOTSPOT_CURRENT_X, unsigned maxx = HOTSPOT_CURRENT_X, unsigned y = HOTSPOT_CURRENT_Y);
+	struct mouse_hotspot* add_hotspot(uint num, bool hungry = true, unsigned minx = HOTSPOT_CURRENT_X, unsigned maxx = HOTSPOT_CURRENT_X, unsigned y = HOTSPOT_CURRENT_Y);
+	struct mouse_hotspot* add_hotspot(const char* cmd, bool hungry = true, unsigned minx = HOTSPOT_CURRENT_X, unsigned maxx = HOTSPOT_CURRENT_X, unsigned y = HOTSPOT_CURRENT_Y);
+	bool add_pause_hotspot(char cmd);
+	void inc_row(unsigned count = 1);
+	void inc_column(unsigned count = 1);
+	void dec_row(unsigned count = 1);
+	void dec_column(unsigned count = 1);
+	void set_row(unsigned val = 0);
+	void set_column(unsigned val = 0);
+	void cond_newline();
+	void cond_blankline();
+	void cond_contline();
+	bool supports(unsigned cmp_flags);
+	bool supports_any(unsigned cmp_flags);
+	uint32_t charset();
+	const char *charset_str();
+	list_node_t *find_hotspot(unsigned x, unsigned y);
+	uint32_t flags(bool raw = false);
+	void insert_indicator();
+	char *attrstr(unsigned newattr, char *str, size_t strsz);
+	bool utf8_increment(unsigned char ch);
+};
+
+void update_terminal(sbbs_t *sbbsptr, Terminal *term);
+void update_terminal(sbbs_t *sbbsptr);
+
+extern "C" {
+#endif
+
+void update_terminal(void *sbbsptr, user_t *userptr);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/sbbs3/un_rep.cpp b/src/sbbs3/un_rep.cpp
index 2d6759f0464f1d5d8e5789ca7c26f1dfce6fcda1..39ab25007afb0aec462fb5500354377820c0e222 100644
--- a/src/sbbs3/un_rep.cpp
+++ b/src/sbbs3/un_rep.cpp
@@ -193,7 +193,7 @@ bool sbbs_t::unpack_rep(char* repfile)
 			break;
 		}
 
-		lncntr = 0;                   /* defeat pause */
+		term->lncntr = 0;                   /* defeat pause */
 		if (fseek(rep, l, SEEK_SET) != 0) {
 			errormsg(WHERE, ERR_SEEK, msg_fname, l);
 			errors++;
diff --git a/src/sbbs3/upload.cpp b/src/sbbs3/upload.cpp
index 0a30133a10928dbe78a4b41f27cd9ce60ce9c4a8..99e8d4ba8cde0e6eac09b3548642f548535c98b1 100644
--- a/src/sbbs3/upload.cpp
+++ b/src/sbbs3/upload.cpp
@@ -73,7 +73,7 @@ bool sbbs_t::uploadfile(file_t* f)
 			}
 			// Note: str (%s) is path/to/sbbsfile.des (used to be the description itself)
 			int result = external(cmdstr(cfg.ftest[i]->cmd, path, str, NULL, cfg.ftest[i]->ex_mode), cfg.ftest[i]->ex_mode | EX_OFFLINE);
-			clearline();
+			term->clearline();
 			if (result != 0) {
 				safe_snprintf(str, sizeof(str), "attempted to upload %s to %s %s (%s error code %d)"
 				              , f->name
diff --git a/src/sbbs3/useredit.cpp b/src/sbbs3/useredit.cpp
index eddc505f0c8ae7079745aea518b8998185ddcecb..1413aebf2003047462534e2063bc1aa4e1b3a94d 100644
--- a/src/sbbs3/useredit.cpp
+++ b/src/sbbs3/useredit.cpp
@@ -78,7 +78,7 @@ void sbbs_t::useredit(int usernumber)
 		}
 		char   user_pass[LEN_PASS + 1];
 		SAFECOPY(user_pass, user.pass);
-		size_t max_len = cols < 60 ? 8 : cols - 60;
+		size_t max_len = term->cols < 60 ? 8 : term->cols - 60;
 		if (strlen(user_pass) > max_len - 2)
 			SAFEPRINTF2(user_pass, "%.*s..", (int)(max_len - 2), user.pass);
 		bprintf(text[UeditAliasPassword]
@@ -142,18 +142,18 @@ void sbbs_t::useredit(int usernumber)
 		bprintf(text[UeditFlags], u32toaf(user.flags1, tmp), u32toaf(user.flags3, tmp2)
 		        , u32toaf(user.flags2, tmp3), u32toaf(user.flags4, str));
 		bprintf(text[UeditExempts], u32toaf(user.exempt, tmp), u32toaf(user.rest, tmp2));
-		if (lncntr >= rows - 2)
-			lncntr = 0;
+		if (term->lncntr >= term->rows - 2)
+			term->lncntr = 0;
 		if (user.misc & DELETED) {
 			if(user.deldate)
 				datestr(user.deldate, tmp);
 			else
 				datestr(user.laston, tmp);
 			snprintf(str, sizeof str, text[DeletedUser], tmp);
-			center(str);
+			term->center(str);
 		}
 		else if (user.misc & INACTIVE)
-			center(text[InactiveUser]);
+			term->center(text[InactiveUser]);
 		else
 			CRLF;
 		l = lastuser(&cfg);
@@ -168,7 +168,7 @@ void sbbs_t::useredit(int usernumber)
 			continue;
 		}
 		if (IS_ALPHA(l))
-			newline();
+			term->newline();
 		switch (l) {
 			case 'A':
 				bputs(text[EnterYourAlias]);
@@ -336,7 +336,7 @@ void sbbs_t::useredit(int usernumber)
 				editfile(str);
 				break;
 			case 'I':
-				lncntr = 0;
+				term->lncntr = 0;
 				user_config(&user);
 				break;
 			case 'J':   /* Edit Minutes */
@@ -425,7 +425,7 @@ void sbbs_t::useredit(int usernumber)
 				putuserstr(user.number, USER_PHONE, user.phone);
 				break;
 			case 'Q':
-				lncntr = 0;
+				term->lncntr = 0;
 				CLS;
 				free(ar);   /* assertion here */
 				return;
@@ -754,127 +754,127 @@ void sbbs_t::user_config(user_t* user)
 			load_user_text();
 		}
 		SAFECOPY(keys, "Q\r");
-		long term = (user == &useron) ? term_supports() : user->misc;
+		long termf = (user == &useron) ? term->flags() : user->misc;
 		if (*text[UserDefaultsTerminal]) {
-			add_hotspot('T');
+			term->add_hotspot('T');
 			SAFECAT(keys, "T");
-			bprintf(text[UserDefaultsTerminal], term_type(user, term, str, sizeof str));
+			bprintf(text[UserDefaultsTerminal], term_type(user, termf, str, sizeof str));
 		}
 		if (*text[UserDefaultsRows]) {
-			add_hotspot('L');
+			term->add_hotspot('L');
 			SAFECAT(keys, "L");
 			bprintf(text[UserDefaultsRows], term_cols(user, str, sizeof str), term_rows(user, tmp, sizeof tmp));
 		}
 		if (*text[UserDefaultsCommandSet] && cfg.total_shells > 1) {
-			add_hotspot('K');
+			term->add_hotspot('K');
 			SAFECAT(keys, "K");
 			bprintf(text[UserDefaultsCommandSet], cfg.shell[user->shell]->name);
 		}
 		if (*text[UserDefaultsLanguage] && get_lang_count(&cfg) > 1) {
-			add_hotspot('I');
+			term->add_hotspot('I');
 			SAFECAT(keys, "I");
 			bprintf(text[UserDefaultsLanguage], text[Language], text[LANG]);
 		}
 		if (*text[UserDefaultsXeditor] && cfg.total_xedits) {
-			add_hotspot('E');
+			term->add_hotspot('E');
 			SAFECAT(keys, "E");
 			bprintf(text[UserDefaultsXeditor]
 			        , user->xedit ? cfg.xedit[user->xedit - 1]->name : text[None]);
 		}
 		if (*text[UserDefaultsArcType]) {
-			add_hotspot('A');
+			term->add_hotspot('A');
 			SAFECAT(keys, "A");
 			bprintf(text[UserDefaultsArcType]
 			        , user->tmpext);
 		}
 		if (*text[UserDefaultsMenuMode]) {
-			add_hotspot('X');
+			term->add_hotspot('X');
 			SAFECAT(keys, "X");
 			bprintf(text[UserDefaultsMenuMode]
 			        , user->misc & EXPERT ? text[On] : text[Off]);
 		}
 		if (*text[UserDefaultsPause]) {
-			add_hotspot('P');
+			term->add_hotspot('P');
 			SAFECAT(keys, "P");
 			bprintf(text[UserDefaultsPause]
 			        , user->misc & UPAUSE ? text[On] : text[Off]);
 		}
 		if (*text[UserDefaultsHotKey]) {
-			add_hotspot('H');
+			term->add_hotspot('H');
 			SAFECAT(keys, "H");
 			bprintf(text[UserDefaultsHotKey]
 			        , user->misc & COLDKEYS ? text[Off] : text[On]);
 		}
 		if (*text[UserDefaultsCursor]) {
-			add_hotspot('S');
+			term->add_hotspot('S');
 			SAFECAT(keys, "S");
 			bprintf(text[UserDefaultsCursor]
 			        , user->misc & SPIN ? text[On] : user->misc & NOPAUSESPIN ? text[Off] : "Pause Prompt Only");
 		}
 		if (*text[UserDefaultsCLS]) {
-			add_hotspot('C');
+			term->add_hotspot('C');
 			SAFECAT(keys, "C");
 			bprintf(text[UserDefaultsCLS]
 			        , user->misc & CLRSCRN ? text[On] : text[Off]);
 		}
 		if (*text[UserDefaultsAskNScan]) {
-			add_hotspot('N');
+			term->add_hotspot('N');
 			SAFECAT(keys, "N");
 			bprintf(text[UserDefaultsAskNScan]
 			        , user->misc & ASK_NSCAN ? text[On] : text[Off]);
 		}
 		if (*text[UserDefaultsAskSScan]) {
-			add_hotspot('Y');
+			term->add_hotspot('Y');
 			SAFECAT(keys, "Y");
 			bprintf(text[UserDefaultsAskSScan]
 			        , user->misc & ASK_SSCAN ? text[On] : text[Off]);
 		}
 		if (*text[UserDefaultsANFS]) {
-			add_hotspot('F');
+			term->add_hotspot('F');
 			SAFECAT(keys, "F");
 			bprintf(text[UserDefaultsANFS]
 			        , user->misc & ANFSCAN ? text[On] : text[Off]);
 		}
 		if (*text[UserDefaultsRemember]) {
-			add_hotspot('R');
+			term->add_hotspot('R');
 			SAFECAT(keys, "R");
 			bprintf(text[UserDefaultsRemember]
 			        , user->misc & CURSUB ? text[On] : text[Off]);
 		}
 		if (*text[UserDefaultsBatFlag]) {
-			add_hotspot('B');
+			term->add_hotspot('B');
 			SAFECAT(keys, "B");
 			bprintf(text[UserDefaultsBatFlag]
 			        , user->misc & BATCHFLAG ? text[On] : text[Off]);
 		}
 		if (*text[UserDefaultsNetMail] && (cfg.sys_misc & SM_FWDTONET)) {
-			add_hotspot('M');
+			term->add_hotspot('M');
 			SAFECAT(keys, "M");
 			bprintf(text[UserDefaultsNetMail]
 			        , user->misc & NETMAIL ? text[On] : text[Off]
 			        , user->netmail);
 		}
 		if (*text[UserDefaultsQuiet] && (user->exempt & FLAG('Q') || user->misc & QUIET)) {
-			add_hotspot('D');
+			term->add_hotspot('D');
 			SAFECAT(keys, "D");
 			bprintf(text[UserDefaultsQuiet]
 			        , user->misc & QUIET ? text[On] : text[Off]);
 		}
 		if (*text[UserDefaultsProtocol]) {
-			add_hotspot('Z');
+			term->add_hotspot('Z');
 			SAFECAT(keys, "Z");
 			bprintf(text[UserDefaultsProtocol], protname(user->prot, XFER_DOWNLOAD)
 			        , user->misc & AUTOHANG ? "(Auto-Hangup)" : nulstr);
 		}
 		if (*text[UserDefaultsPassword] && (cfg.sys_misc & SM_PWEDIT) && !(user->rest & FLAG('G'))) {
-			add_hotspot('W');
+			term->add_hotspot('W');
 			SAFECAT(keys, "W");
 			bputs(text[UserDefaultsPassword]);
 		}
 
 		sync();
 		bputs(text[UserDefaultsWhich]);
-		add_hotspot('Q');
+		term->add_hotspot('Q');
 		ch = (char)getkeys(keys, 0);
 		switch (ch) {
 			case 'T':
@@ -906,8 +906,8 @@ void sbbs_t::user_config(user_t* user)
 				}
 				if (sys_status & SS_ABORT)
 					break;
-				term = (user == &useron) ? term_supports() : user->misc;
-				if (term & (AUTOTERM | ANSI) && !(term & PETSCII)) {
+				termf = (user == &useron) ? term->flags() : user->misc;
+				if (termf & (AUTOTERM | ANSI) && !(termf & PETSCII)) {
 					user->misc |= COLOR;
 					user->misc &= ~ICE_COLOR;
 					if ((user->misc & AUTOTERM) || yesno(text[ColorTerminalQ])) {
@@ -919,7 +919,7 @@ void sbbs_t::user_config(user_t* user)
 				}
 				if (sys_status & SS_ABORT)
 					break;
-				if (term & ANSI) {
+				if (termf & ANSI) {
 					if (text[MouseTerminalQ][0] && yesno(text[MouseTerminalQ]))
 						user->misc |= MOUSE;
 					else
@@ -927,8 +927,8 @@ void sbbs_t::user_config(user_t* user)
 				}
 				if (sys_status & SS_ABORT)
 					break;
-				if (!(term & PETSCII)) {
-					if (!(term & UTF8) && !yesno(text[ExAsciiTerminalQ]))
+				if (!(termf & PETSCII)) {
+					if (!(termf & UTF8) && !yesno(text[ExAsciiTerminalQ]))
 						user->misc |= NO_EXASCII;
 					else
 						user->misc &= ~NO_EXASCII;
@@ -946,7 +946,7 @@ void sbbs_t::user_config(user_t* user)
 						else if (key == PETSCII_DELETE) {
 							autoterm |= PETSCII;
 							user->misc |= PETSCII;
-							outcom(PETSCII_UPPERLOWER);
+							term_out(PETSCII_UPPERLOWER);
 							bputs(text[PetTerminalDetected]);
 						}
 						else
@@ -955,7 +955,7 @@ void sbbs_t::user_config(user_t* user)
 				}
 				if (sys_status & SS_ABORT)
 					break;
-				if (!(user->misc & AUTOTERM) && (term & (ANSI | NO_EXASCII)) == ANSI) {
+				if (!(user->misc & AUTOTERM) && (termf & (ANSI | NO_EXASCII)) == ANSI) {
 					if (!noyes(text[RipTerminalQ]))
 						user->misc |= RIP;
 					else
@@ -1166,7 +1166,7 @@ void sbbs_t::user_config(user_t* user)
 				putusermisc(user->number, user->misc);
 				break;
 			default:
-				clear_hotspots();
+				term->clear_hotspots();
 				return;
 		}
 	}
diff --git a/src/sbbs3/ver.cpp b/src/sbbs3/ver.cpp
index b137fbdb514488f40a50c8c2118a413a5b5a92b2..ca73fdaa875af29118d1208b8dc41791f8311ea0 100644
--- a/src/sbbs3/ver.cpp
+++ b/src/sbbs3/ver.cpp
@@ -76,14 +76,16 @@ char* socklib_version(char* str, size_t size, char* winsock_ver)
 void sbbs_t::ver()
 {
 	char str[128], compiler[32], os[128], cpu[128];
+#ifdef USE_MOSQUITTO
 	char tmp[128];
+#endif
 
 	CRLF;
 	strcpy(str, VERSION_NOTICE);
 #if defined(_DEBUG)
 	strcat(str, "  Debug");
 #endif
-	center(str);
+	term->center(str);
 	CRLF;
 
 	DESCRIBE_COMPILER(compiler);
@@ -95,19 +97,19 @@ void sbbs_t::ver()
 	         , git_date
 	         , smb_lib_ver(), compiler);
 
-	center(str);
+	term->center(str);
 	CRLF;
 
-	center("https://gitlab.synchro.net - " GIT_BRANCH "/" GIT_HASH);
+	term->center("https://gitlab.synchro.net - " GIT_BRANCH "/" GIT_HASH);
 	CRLF;
 
 	snprintf(str, sizeof str, "%s - http://synchro.net", COPYRIGHT_NOTICE);
-	center(str);
+	term->center(str);
 	CRLF;
 
 #ifdef JAVASCRIPT
 	if (!(startup->options & BBS_OPT_NO_JAVASCRIPT)) {
-		center((char *)JS_GetImplementationVersion());
+		term->center((char *)JS_GetImplementationVersion());
 		CRLF;
 	}
 #endif
@@ -121,24 +123,24 @@ void sbbs_t::ver()
 		result = cryptGetAttribute(CRYPT_UNUSED, CRYPT_OPTION_INFO_STEPPING, &cl_step);
 		(void)result;
 		safe_snprintf(str, sizeof(str), "cryptlib %u.%u.%u (%u)", cl_major, cl_minor, cl_step, CRYPTLIB_VERSION);
-		center(str);
+		term->center(str);
 		CRLF;
 	}
 #endif
 
 	safe_snprintf(str, sizeof str, "%s (%u)", archive_version_string(), ARCHIVE_VERSION_NUMBER);
-	center(str);
+	term->center(str);
 	CRLF;
 
 #ifdef USE_MOSQUITTO
 	SAFECOPY(str, mqtt_libver(tmp, sizeof tmp));
 	safe_snprintf(tmp, sizeof tmp, " (%u)", LIBMOSQUITTO_VERSION_NUMBER);
 	SAFECAT(str, tmp);
-	center(str);
+	term->center(str);
 	CRLF;
 #endif
 
 	safe_snprintf(str, sizeof(str), "%s %s", os_version(os, sizeof(os)), os_cpuarch(cpu, sizeof(cpu)));
-	center(str);
+	term->center(str);
 }
 #endif
diff --git a/src/sbbs3/writemsg.cpp b/src/sbbs3/writemsg.cpp
index c3edb51a921c16acb1a0393873074a6b4097f4ab..3f9f45449e8ba0313fa89952973e31548b2286d1 100644
--- a/src/sbbs3/writemsg.cpp
+++ b/src/sbbs3/writemsg.cpp
@@ -25,7 +25,7 @@
 #include "git_branch.h"
 #include "git_hash.h"
 
-#define MAX_LINE_LEN    ((cols - 1) + 2)
+#define MAX_LINE_LEN    ((term->cols - 1) + 2)
 
 const char *quote_fmt = " > %.*s\r\n";
 void quotestr(char *str);
@@ -97,7 +97,7 @@ bool sbbs_t::quotemsg(smb_t* smb, smbmsg_t* msg, bool tails)
 		BOOL is_utf8 = FALSE;
 		if (!str_is_ascii(buf)) {
 			if (smb_msg_is_utf8(msg)) {
-				if (term_supports(UTF8)
+				if ((term->charset() == CHARSET_UTF8)
 				    && (!useron_xedit || (cfg.xedit[useron_xedit - 1]->misc & XTRN_UTF8)))
 					is_utf8 = TRUE;
 				else {
@@ -105,7 +105,7 @@ bool sbbs_t::quotemsg(smb_t* smb, smbmsg_t* msg, bool tails)
 				}
 			} else { // CP437
 				char* orgtxt;
-				if (term_supports(UTF8)
+				if ((term->charset() == CHARSET_UTF8)
 				    && (!useron_xedit || (cfg.xedit[useron_xedit - 1]->misc & XTRN_UTF8))
 				    && (orgtxt = strdup(buf)) != NULL) {
 					is_utf8 = TRUE;
@@ -124,7 +124,7 @@ bool sbbs_t::quotemsg(smb_t* smb, smbmsg_t* msg, bool tails)
 			if (useron_xedit > 0)
 				wrap_cols = cfg.xedit[useron_xedit - 1]->quotewrap_cols;
 			if (wrap_cols == 0)
-				wrap_cols = cols - 1;
+				wrap_cols = term->cols - 1;
 			wrapped = ::wordwrap(buf, wrap_cols, org_cols - 1, /* handle_quotes: */ TRUE, is_utf8, /* pipe_codes: */false);
 		}
 		if (wrapped != NULL) {
@@ -291,8 +291,8 @@ bool sbbs_t::writemsg(const char *fname, const char *top, char *subj, int mode,
 	unsigned lines;
 	ushort   useron_xedit = useron.xedit;
 
-	if (cols < TERM_COLS_MIN) {
-		errormsg(WHERE, ERR_CHK, "columns (too narrow)", cols);
+	if (term->cols < TERM_COLS_MIN) {
+		errormsg(WHERE, ERR_CHK, "columns (too narrow)", term->cols);
 		return false;
 	}
 
@@ -366,7 +366,7 @@ bool sbbs_t::writemsg(const char *fname, const char *top, char *subj, int mode,
 				if (!fgets(str, sizeof(str), stream))
 					break;
 				quotestr(str);
-				SAFEPRINTF2(tmp, quote_fmt, cols - 4, str);
+				SAFEPRINTF2(tmp, quote_fmt, term->cols - 4, str);
 				if (write(file, tmp, strlen(tmp)) > 0)
 					linesquoted++;
 			}
@@ -422,7 +422,7 @@ bool sbbs_t::writemsg(const char *fname, const char *top, char *subj, int mode,
 						if (!fgets(str, sizeof(str), stream))
 							break;
 						quotestr(str);
-						SAFEPRINTF2(tmp, quote_fmt, cols - 4, str);
+						SAFEPRINTF2(tmp, quote_fmt, term->cols - 4, str);
 						if (write(file, tmp, strlen(tmp)) > 0)
 							linesquoted++;
 					}
@@ -437,7 +437,7 @@ bool sbbs_t::writemsg(const char *fname, const char *top, char *subj, int mode,
 						if (!fgets(str, sizeof(str), stream))
 							break;
 						quotestr(str);
-						bprintf(P_AUTO_UTF8, "%4d: %.*s\r\n", i, (int)cols - 7, str);
+						bprintf(P_AUTO_UTF8, "%4d: %.*s\r\n", i, (int)term->cols - 7, str);
 						i++;
 					}
 					continue;
@@ -466,7 +466,7 @@ bool sbbs_t::writemsg(const char *fname, const char *top, char *subj, int mode,
 							if (!fgets(str, sizeof(str), stream))
 								break;
 							quotestr(str);
-							SAFEPRINTF2(tmp, quote_fmt, cols - 4, str);
+							SAFEPRINTF2(tmp, quote_fmt, term->cols - 4, str);
 							if (write(file, tmp, strlen(tmp)) > 0)
 								linesquoted++;
 							j++;
@@ -475,7 +475,7 @@ bool sbbs_t::writemsg(const char *fname, const char *top, char *subj, int mode,
 					else {          /* one line */
 						if (fgets(str, sizeof(str), stream)) {
 							quotestr(str);
-							SAFEPRINTF2(tmp, quote_fmt, cols - 4, str);
+							SAFEPRINTF2(tmp, quote_fmt, term->cols - 4, str);
 							if (write(file, tmp, strlen(tmp)) > 0)
 								linesquoted++;
 						}
@@ -508,7 +508,7 @@ bool sbbs_t::writemsg(const char *fname, const char *top, char *subj, int mode,
 		else {
 			bputs(text[SubjectPrompt]);
 		}
-		max_title_len = cols - column - 1;
+		max_title_len = term->cols - term->column - 1;
 		if (max_title_len > LEN_TITLE)
 			max_title_len = LEN_TITLE;
 		if (draft_restored)
@@ -584,11 +584,11 @@ bool sbbs_t::writemsg(const char *fname, const char *top, char *subj, int mode,
 			*editor = cfg.xedit[useron_xedit - 1]->name;
 		if (!str_is_ascii(subj)) {
 			if (utf8_str_is_valid(subj)) {
-				if (!term_supports(UTF8) || !(cfg.xedit[useron_xedit - 1]->misc & XTRN_UTF8)) {
+				if ((term->charset() != CHARSET_UTF8) || !(cfg.xedit[useron_xedit - 1]->misc & XTRN_UTF8)) {
 					utf8_to_cp437_inplace(subj);
 				}
 			} else { // CP437
-				if (term_supports(UTF8) && (cfg.xedit[useron_xedit - 1]->misc & XTRN_UTF8)) {
+				if ((term->charset() == CHARSET_UTF8) && (cfg.xedit[useron_xedit - 1]->misc & XTRN_UTF8)) {
 					cp437_to_utf8_str(subj, str, sizeof(str) - 1, /* minval: */ '\x80');
 					safe_snprintf(subj, LEN_TITLE + 1, "%s", str);
 				}
@@ -704,7 +704,7 @@ bool sbbs_t::writemsg(const char *fname, const char *top, char *subj, int mode,
 		if (linesquoted || draft_restored) {
 			if ((file = nopen(msgtmp, O_RDONLY)) != -1) {
 				length = (long)filelength(file);
-				l = length > (cfg.level_linespermsg[useron_level] * MAX_LINE_LEN) - 1
+				l = length > (int)(cfg.level_linespermsg[useron_level] * MAX_LINE_LEN) - 1
 				    ? (cfg.level_linespermsg[useron_level] * MAX_LINE_LEN) - 1 : length;
 				if (read(file, buf, l) != l)
 					l = 0;
@@ -813,7 +813,7 @@ void sbbs_t::editor_info_to_msg(smbmsg_t* msg, const char* editor, const char* c
 		useron_xedit = 0;
 
 	if (editor == NULL || useron_xedit == 0 || (cfg.xedit[useron_xedit - 1]->misc & SAVECOLUMNS))
-		smb_hfield_bin(msg, SMB_COLUMNS, cols);
+		smb_hfield_bin(msg, SMB_COLUMNS, term->cols);
 
 	if (!str_is_ascii(msg->subj) && utf8_str_is_valid(msg->subj))
 		msg->hdr.auxattr |= MSG_HFIELDS_UTF8;
@@ -958,8 +958,8 @@ uint sbbs_t::msgeditor(char *buf, const char *top, char *title, uint maxlines, u
 	str_list_t str;
 	long       pmode = P_SAVEATR | P_NOATCODES | P_AUTO_UTF8;
 
-	if (cols < TERM_COLS_MIN) {
-		errormsg(WHERE, ERR_CHK, "columns (too narrow)", cols);
+	if (term->cols < TERM_COLS_MIN) {
+		errormsg(WHERE, ERR_CHK, "columns (too narrow)", term->cols);
 		return 0;
 	}
 
@@ -980,8 +980,8 @@ uint sbbs_t::msgeditor(char *buf, const char *top, char *title, uint maxlines, u
 	bprintf(text[EnterMsgNow], maxlines);
 
 	if (!menu("msgtabs", P_NOERROR)) {
-		for (i = 0; i < (cols - 1); i++) {
-			if (i % EDIT_TABSIZE || !i)
+		for (unsigned u = 0; u < (term->cols - 1); u++) {
+			if (u % EDIT_TABSIZE || !u)
 				outchar('-');
 			else
 				outchar('+');
@@ -991,7 +991,7 @@ uint sbbs_t::msgeditor(char *buf, const char *top, char *title, uint maxlines, u
 	putmsg(top, pmode);
 	for (line = 0; line < lines && !msgabort(); line++) { /* display lines in buf */
 		putmsg(str[line], pmode);
-		cleartoeol();  /* delete to end of line */
+		term->cleartoeol();  /* delete to end of line */
 		CRLF;
 	}
 	sync();
@@ -1012,14 +1012,14 @@ uint sbbs_t::msgeditor(char *buf, const char *top, char *title, uint maxlines, u
 			else
 				strin[0] = 0;
 			if (line < 1)
-				carriage_return();
+				term->carriage_return();
 			ulong prev_con = console;
 			int   kmode = K_WORDWRAP | K_MSG | K_EDIT | K_NOCRLF | K_USEOFFSET;
 			if (line)
 				kmode |= K_LEFTEXIT;
 			if (str[line] != NULL)
 				kmode |= K_RIGHTEXIT;
-			getstr(strin, cols - 1, kmode);
+			getstr(strin, term->cols - 1, kmode);
 			if ((prev_con & CON_DELETELINE) /* Ctrl-X/ZDLE */ && strncmp(strin, "B00", 3) == 0) {
 				strin[0] = 0;
 				prot = 'Z';
@@ -1035,13 +1035,13 @@ uint sbbs_t::msgeditor(char *buf, const char *top, char *title, uint maxlines, u
 				strListRemove(&str, line);
 				for (i = line; str[i]; i++) {
 					putmsg(str[i], pmode);
-					cleartoeol();
-					newline();
+					term->cleartoeol();
+					term->newline();
 				}
-				clearline();
+				term->clearline();
 				if (line)
 					--line;
-				cursor_up(i - line);
+				term->cursor_up(i - line);
 				continue;
 			} else if (str[line] == NULL) {
 				if (strin[0] != 0)
@@ -1050,9 +1050,9 @@ uint sbbs_t::msgeditor(char *buf, const char *top, char *title, uint maxlines, u
 				strListReplace(str, line, strin);
 			if (line < 1)
 				continue;
-			carriage_return();
-			cursor_up();
-			cleartoeol();
+			term->carriage_return();
+			term->cursor_up();
+			term->cleartoeol();
 			line--;
 			continue;
 		}
@@ -1060,7 +1060,7 @@ uint sbbs_t::msgeditor(char *buf, const char *top, char *title, uint maxlines, u
 			strListDelete(&str, line);
 			continue;
 		}
-		newline();
+		term->newline();
 		if (console & (CON_DOWNARROW | CON_RIGHTARROW)) {
 			if (str[line] != NULL) {
 				strListReplace(str, line, strin);
@@ -1070,7 +1070,7 @@ uint sbbs_t::msgeditor(char *buf, const char *top, char *title, uint maxlines, u
 		}
 		if (strin[0] == '/' && strlen(strin) < 16) {
 			if (!stricmp(strin, "/DEBUG") && SYSOP) {
-				bprintf("\r\nline=%d lines=%d (%d), rows=%d\r\n", line, lines, (int)strListCount(str), rows);
+				bprintf("\r\nline=%d lines=%d (%d), rows=%d\r\n", line, lines, (int)strListCount(str), term->rows);
 				continue;
 			}
 			else if (!stricmp(strin, "/ABT")) {
@@ -1137,7 +1137,7 @@ uint sbbs_t::msgeditor(char *buf, const char *top, char *title, uint maxlines, u
 					bputs(text[InvalidLineNumber]);
 				else {
 					SAFECOPY(strin, str[i]);
-					getstr(strin, cols - 1, j);
+					getstr(strin, term->cols - 1, j);
 					strListReplace(str, i, strin);
 				}
 				continue;
@@ -1169,12 +1169,12 @@ uint sbbs_t::msgeditor(char *buf, const char *top, char *title, uint maxlines, u
 				int  digits = DEC_DIGITS(lines);
 				while (str[j] != NULL && !msgabort()) {
 					if (linenums) { /* line numbers */
-						snprintf(tmp, sizeof tmp, "%*d: %-.*s", digits, j + 1, (int)(cols - (digits + 3)), str[j]);
+						snprintf(tmp, sizeof tmp, "%*d: %-.*s", digits, j + 1, (int)(term->cols - (digits + 3)), str[j]);
 						putmsg(tmp, pmode);
 					}
 					else
 						putmsg(str[j], pmode);
-					cleartoeol();  /* delete to end of line */
+					term->cleartoeol();  /* delete to end of line */
 					CRLF;
 					j++;
 				}
@@ -1238,11 +1238,11 @@ upload:
 			strListInsert(&str, "", line);
 			for (i = line; str[i]; i++) {
 				putmsg(str[i], pmode);
-				cleartoeol();
-				newline();
+				term->cleartoeol();
+				term->newline();
 			}
-			clearline();
-			cursor_up(i - line);
+			term->clearline();
+			term->cursor_up(i - line);
 			continue;
 		}
 
@@ -1283,8 +1283,8 @@ bool sbbs_t::editfile(char *fname, uint maxlines, const char* to, const char* fr
 	unsigned lines;
 	ushort   useron_xedit = useron.xedit;
 
-	if (cols < TERM_COLS_MIN) {
-		errormsg(WHERE, ERR_CHK, "columns (too narrow)", cols);
+	if (term->cols < TERM_COLS_MIN) {
+		errormsg(WHERE, ERR_CHK, "columns (too narrow)", term->cols);
 		return false;
 	}
 
diff --git a/src/sbbs3/xtrn.cpp b/src/sbbs3/xtrn.cpp
index 5587286b18c33085e1ae564108eda13a13a657d2..8b19107d5cf635c537170510b8813d23e7dafe3f 100644
--- a/src/sbbs3/xtrn.cpp
+++ b/src/sbbs3/xtrn.cpp
@@ -385,7 +385,7 @@ int sbbs_t::external(const char* cmdline, int mode, const char* startup_dir)
 		return -1;
 	}
 
-	clear_hotspots();
+	term->clear_hotspots();
 
 	XTRN_LOADABLE_MODULE(cmdline, startup_dir);
 	XTRN_LOADABLE_JS_MODULE(cmdline, mode, startup_dir);
@@ -1146,7 +1146,7 @@ int sbbs_t::external(const char* cmdline, int mode, const char* startup_dir)
 	xtrn_mode = mode;
 	lprintf(LOG_DEBUG, "Executing external: %s", cmdline);
 
-	clear_hotspots();
+	term->clear_hotspots();
 
 	if (startup_dir == NULL)
 		startup_dir = nulstr;
@@ -1617,17 +1617,17 @@ int sbbs_t::external(const char* cmdline, int mode, const char* startup_dir)
 
 	if ((mode & EX_STDIO) == EX_STDIO)  {
 		struct winsize winsize;
-		struct termios term;
-		memset(&term, 0, sizeof(term));
-		cfsetispeed(&term, B19200);
-		cfsetospeed(&term, B19200);
+		struct termios termio;
+		memset(&termio, 0, sizeof(term));
+		cfsetispeed(&termio, B19200);
+		cfsetospeed(&termio, B19200);
 		if (mode & EX_BIN)
-			cfmakeraw(&term);
+			cfmakeraw(&termio);
 		else {
-			term.c_iflag = TTYDEF_IFLAG;
-			term.c_oflag = TTYDEF_OFLAG;
-			term.c_lflag = TTYDEF_LFLAG;
-			term.c_cflag = TTYDEF_CFLAG;
+			termio.c_iflag = TTYDEF_IFLAG;
+			termio.c_oflag = TTYDEF_OFLAG;
+			termio.c_lflag = TTYDEF_LFLAG;
+			termio.c_cflag = TTYDEF_CFLAG;
 			/*
 			 * On Linux, ttydefchars is in the wrong order, so
 			 * it's completely useless for anything.
@@ -1635,89 +1635,89 @@ int sbbs_t::external(const char* cmdline, int mode, const char* startup_dir)
 			 * to a value we may have made up.
 			 * TODO: We can set stuff from the user term here...
 			 */
-			for (unsigned noti = 0; noti < (sizeof(term.c_cc) / sizeof(term.c_cc[0])); noti++)
-				term.c_cc[noti] = _POSIX_VDISABLE;
+			for (unsigned noti = 0; noti < (sizeof(termio.c_cc) / sizeof(termio.c_cc[0])); noti++)
+				termio.c_cc[noti] = _POSIX_VDISABLE;
 #ifdef VEOF
-			term.c_cc[VEOF] = CEOF;
+			termio.c_cc[VEOF] = CEOF;
 #endif
 #ifdef VEOL
-			term.c_cc[VEOL] = CEOL;
+			termio.c_cc[VEOL] = CEOL;
 #endif
 #ifdef VEOL2
 #ifdef CEOL2
-			term.c_cc[VEOL2] = CEOL2;
+			termio.c_cc[VEOL2] = CEOL2;
 #else
-			term.c_cc[VEOL2] = CEOL;
+			termio.c_cc[VEOL2] = CEOL;
 #endif
 #endif
 #ifdef VERASE
-			term.c_cc[VERASE] = CERASE;
+			termio.c_cc[VERASE] = CERASE;
 #endif
 #ifdef VKILL
-			term.c_cc[VKILL] = CKILL;
+			termio.c_cc[VKILL] = CKILL;
 #endif
 #ifdef VREPRINT
-			term.c_cc[VREPRINT] = CREPRINT;
+			termio.c_cc[VREPRINT] = CREPRINT;
 #endif
 #ifdef VINTR
-			term.c_cc[VINTR] = CINTR;
+			termio.c_cc[VINTR] = CINTR;
 #endif
 #ifdef VERASE2
 #ifdef CERASE2
-			term.c_cc[VERASE2] = CERASE2;
+			termio.c_cc[VERASE2] = CERASE2;
 #else
-			term.c_cc[VERASE2] = CERASE;
+			termio.c_cc[VERASE2] = CERASE;
 #endif
 #endif
 #ifdef VQUIT
-			term.c_cc[VQUIT] = CQUIT;
+			termio.c_cc[VQUIT] = CQUIT;
 #endif
 #ifdef VSUSP
-			term.c_cc[VSUSP] = CSUSP;
+			termio.c_cc[VSUSP] = CSUSP;
 #endif
 #ifdef VDSUSP
-			term.c_cc[VDSUSP] = CDSUSP;
+			termio.c_cc[VDSUSP] = CDSUSP;
 #endif
 #ifdef VSTART
-			term.c_cc[VSTART] = CSTART;
+			termio.c_cc[VSTART] = CSTART;
 #endif
 #ifdef VSTOP
-			term.c_cc[VSTOP] = CSTOP;
+			termio.c_cc[VSTOP] = CSTOP;
 #endif
 #ifdef VLNEXT
-			term.c_cc[VLNEXT] = CLNEXT;
+			termio.c_cc[VLNEXT] = CLNEXT;
 #endif
 #ifdef VDISCARD
-			term.c_cc[VDISCARD] = CDISCARD;
+			termio.c_cc[VDISCARD] = CDISCARD;
 #endif
 #ifdef VMIN
-			term.c_cc[VMIN] = CMIN;
+			termio.c_cc[VMIN] = CMIN;
 #endif
 #ifdef VTIME
-			term.c_cc[VTIME] = CTIME;
+			termio.c_cc[VTIME] = CTIME;
 #endif
 #ifdef VSTATUS
-			term.c_cc[VSTATUS] = CSTATUS;
+			termio.c_cc[VSTATUS] = CSTATUS;
 #endif
 #ifdef VWERASE
-			term.c_cc[VWERASE] = CWERASE;
+			termio.c_cc[VWERASE] = CWERASE;
 #endif
 #ifdef VEOT
-			term.c_cc[VEOT] = CEOT;
+			termio.c_cc[VEOT] = CEOT;
 #endif
 #ifdef VBRK
-			term.c_cc[VBRK] = CBRK;
+			termio.c_cc[VBRK] = CBRK;
 #endif
 #ifdef VRPRNT
-			term.c_cc[VRPRNT] = CRPRNT;
+			termio.c_cc[VRPRNT] = CRPRNT;
 #endif
 #ifdef VFLUSH
-			term.c_cc[VFLUSH] = CFLUSH
+			termio.c_cc[VFLUSH] = CFLUSH
 #endif
 		}
-		winsize.ws_row = rows;
-		winsize.ws_col = cols;
-		if ((pid = forkpty(&in_pipe[1], NULL, &term, &winsize)) == -1) {
+		winsize.ws_row = term->rows;
+		winsize.ws_col = term->cols;
+		if ((pid = forkpty(&in_pipe[1], NULL, &termio, &winsize)) == -1) {
 			if (!(mode & (EX_STDIN | EX_OFFLINE))) {
 				if (passthru_thread_running)
 					passthru_socket_activate(false);
@@ -1762,7 +1762,7 @@ int sbbs_t::external(const char* cmdline, int mode, const char* startup_dir)
 		sigfillset(&sigs);
 		sigprocmask(SIG_UNBLOCK, &sigs, NULL);
 		if (!(mode & EX_BIN))  {
-			if (term_supports(ANSI))
+			if (term->supports(ANSI))
 				SAFEPRINTF(term_env, "TERM=%s", startup->xtrn_term_ansi);
 			else
 				SAFEPRINTF(term_env, "TERM=%s", startup->xtrn_term_dumb);
@@ -1971,9 +1971,9 @@ int sbbs_t::external(const char* cmdline, int mode, const char* startup_dir)
 					bp = buf;
 					output_len = rd;
 				}
-				if (term_supports(PETSCII))
+				if (term->charset() == CHARSET_PETSCII)
 					petscii_convert(bp, output_len);
-				else if (term_supports(UTF8))
+				else if (term->charset() == CHARSET_UTF8)
 					bp = cp437_to_utf8(bp, output_len, utf8_buf, sizeof utf8_buf);
 			}
 			/* Did expansion overrun the output buffer? */
@@ -2193,7 +2193,7 @@ char* sbbs_t::cmdstr(const char *instr, const char *fpath, const char *fspec, ch
 					strncat(cmd, cfg.sys_id, avail);
 					break;
 				case 'R':   /* Rows */
-					strncat(cmd, ultoa(rows, str, 10), avail);
+					strncat(cmd, ultoa(term->rows, str, 10), avail);
 					break;
 				case 'S':   /* File Spec (or Baja command str) or startup-directory */
 					strncat(cmd, fspec, avail);
@@ -2210,7 +2210,7 @@ char* sbbs_t::cmdstr(const char *instr, const char *fpath, const char *fspec, ch
 					strncat(cmd, str, avail);
 					break;
 				case 'W':   /* Columns (width) */
-					strncat(cmd, ultoa(cols, str, 10), avail);
+					strncat(cmd, ultoa(term->cols, str, 10), avail);
 					break;
 				case 'X':
 					strncat(cmd, cfg.shell[useron.shell]->code, avail);
diff --git a/src/sbbs3/xtrn_sec.cpp b/src/sbbs3/xtrn_sec.cpp
index f19a9b7d858a0af629a3ab5bed179801c8de015d..61efa76500e7f8b7d729b87f0ca771fcf3a8db5b 100644
--- a/src/sbbs3/xtrn_sec.cpp
+++ b/src/sbbs3/xtrn_sec.cpp
@@ -142,7 +142,6 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, uint tle
 	struct tm tm;
 	struct tm tl;
 	stats_t   stats;
-	int       term = term_supports();
 
 	char      node_dir[MAX_PATH + 1];
 	char      ctrl_dir[MAX_PATH + 1];
@@ -204,10 +203,10 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, uint tle
 		              , cfg.sys_nodes           /* Total system nodes */
 		              , cfg.node_num            /* Current node */
 		              , tleft                   /* User Timeleft in seconds */
-		              , (term & ANSI)           /* User ANSI ? (Yes/Mono/No) */
-		        ? (term & COLOR)
+		              , (term->supports(ANSI))           /* User ANSI ? (Yes/Mono/No) */
+		        ? (term->supports(COLOR))
 		        ? "Yes":"Mono":"No"
-		              , rows                    /* User Screen lines */
+		              , term->rows                    /* User Screen lines */
 		              , user_available_credits(&useron)); /* User Credits */
 		lfexpand(str, misc);
 		fwrite(str, strlen(str), 1, fp);
@@ -321,12 +320,12 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, uint tle
 		              , user_available_credits(&useron) /* Gold */
 		              , TM_MONTH(tm.tm_mon)     /* User last on date (MM/DD/YY) */
 		              , tm.tm_mday, TM_YEAR(tm.tm_year)
-		              , cols                    /* User screen width */
-		              , rows                    /* User screen length */
+		              , term->cols                    /* User screen width */
+		              , term->rows                    /* User screen length */
 		              , useron.level            /* User SL */
 		              , 0                       /* Cosysop? */
 		              , SYSOP                   /* Sysop? (1/0) */
-		              , INT_TO_BOOL(term & ANSI) /* ANSI ? (1/0) */
+		              , term->supports(ANSI) /* ANSI ? (1/0) */
 		              , online == ON_REMOTE);   /* Remote (1/0) */
 		lfexpand(str, misc);
 		fwrite(str, strlen(str), 1, fp);
@@ -408,15 +407,15 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, uint tle
 		              , tm.tm_mday, TM_YEAR(tm.tm_year)
 		              , MIN(tleft, INT16_MAX)   /* 18: User time left in sec */
 		              , MIN((tleft / 60), INT16_MAX) /* 19: User time left in min */
-		              , (term & NO_EXASCII)     /* 20: GR if COLOR ANSI */
-		        ? "7E" : (term & (ANSI | COLOR)) == (ANSI | COLOR) ? "GR" : "NG");
+		              , term->charset() == CHARSET_ASCII     /* 20: GR if COLOR ANSI */
+		        ? "7E" : (term->supports(ANSI | COLOR) ? "GR" : "NG"));
 		lfexpand(str, misc);
 		fwrite(str, strlen(str), 1, fp);
 
 		t = useron.expire;
 		localtime_r(&t, &tm);
 		safe_snprintf(str, sizeof(str), "%u\n%c\n%s\n%u\n%02u/%02u/%02u\n%u\n%c\n%u\n%u\n"
-		              , rows                    /* 21: User screen length */
+		              , term->rows                    /* 21: User screen length */
 		              , (useron.misc & EXPERT) ? 'Y':'N' /* 22: Expert? (Y/N) */
 		              , u32toaf(useron.flags1, tmp2) /* 23: Registered conferences */
 		              , 0                       /* 24: Conference came from */
@@ -454,7 +453,7 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, uint tle
 
 			localtime_r(&ns_time, &tm);
 			safe_snprintf(str, sizeof(str), "%c\n%c\n%u\n%" PRIu32 "\n%02d/%02d/%02d\n"
-			              , (term & (NO_EXASCII | ANSI | COLOR)) == ANSI
+			              , (term->flags() & (NO_EXASCII | ANSI | COLOR)) == ANSI
 			        ? 'Y':'N'                       /* 39: ANSI supported but NG mode */
 			              , 'Y'                     /* 40: Use record locking */
 			              , cfg.color[clr_external] /* 41: BBS default color */
@@ -535,7 +534,7 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, uint tle
 		              , tmp                     /* User's firstname */
 		              , p                       /* User's lastname */
 		              , useron.location         /* User's city */
-		              , INT_TO_BOOL(term & ANSI) /* 1=ANSI 0=ASCII */
+		              , term->supports(ANSI)    /* 1=ANSI 0=ASCII */
 		              , useron.level            /* Security level */
 		              , MIN((tleft / 60), INT16_MAX)); /* Time left in minutes */
 		strupr(str);
@@ -572,7 +571,7 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, uint tle
 			exitinfo.UserInfo.Attrib |= QBBS::USER_ATTRIB_CLRSCRN;
 		if (useron.misc & UPAUSE)
 			exitinfo.UserInfo.Attrib |= QBBS::USER_ATTRIB_MORE;
-		if (term & ANSI)
+		if (term->supports(ANSI))
 			exitinfo.UserInfo.Attrib |= QBBS::USER_ATTRIB_ANSI;
 		if (useron.sex == 'F')
 			exitinfo.UserInfo.Attrib |= QBBS::USER_ATTRIB_FEMALE;
@@ -584,7 +583,7 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, uint tle
 		exitinfo.UserInfo.UpK = (uint16_t)(useron.ulb / 1024UL);
 		exitinfo.UserInfo.DownK = (uint16_t)(useron.dlb / 1024UL);
 		exitinfo.UserInfo.TodayK = (uint16_t)(logon_dlb / 1024UL);
-		exitinfo.UserInfo.ScreenLength = (int16_t)rows;
+		exitinfo.UserInfo.ScreenLength = (int16_t)term->rows;
 		localtime_r(&logontime, &tm);
 		SAFEPRINTF2(tmp, "%02d:%02d", tm.tm_hour, tm.tm_min);
 		exitinfo.LoginTime = tmp;
@@ -596,12 +595,12 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, uint tle
 		exitinfo.WantChat = (sys_status & SS_SYSPAGE);
 		exitinfo.ScreenClear = (useron.misc & CLRSCRN);
 		exitinfo.MorePrompts = (useron.misc & UPAUSE);
-		exitinfo.GraphicsMode = !(term & NO_EXASCII);
+		exitinfo.GraphicsMode = !(term->charset() == CHARSET_ASCII);
 		exitinfo.ExternEdit = (useron.xedit);
-		exitinfo.ScreenLength = (int16_t)rows;
+		exitinfo.ScreenLength = (int16_t)term->rows;
 		exitinfo.MNP_Connect = true;
-		exitinfo.ANSI_Capable = (term & ANSI);
-		exitinfo.RIP_Active = (term & RIP);
+		exitinfo.ANSI_Capable = term->supports(ANSI);
+		exitinfo.RIP_Active = term->supports(RIP);
 
 		fwrite(&exitinfo, sizeof(exitinfo), 1, fp);
 		fclose(fp);
@@ -650,7 +649,7 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, uint tle
 		              , useron.location         /* User location */
 		              , useron.level            /* Security level */
 		              , MIN((tleft / 60), INT16_MAX) /* Time left in min */
-		              , (term & ANSI) ? "COLOR":"MONO" /* ANSI ??? */
+		              , term->supports(ANSI) ? "COLOR":"MONO" /* ANSI ??? */
 		              , useron.pass             /* Password */
 		              , useron.number);         /* User number */
 		lfexpand(str, misc);
@@ -690,7 +689,7 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, uint tle
 		              , TM_MONTH(tm.tm_mon), tm.tm_mday /* File new-scan date */
 		              , TM_YEAR(tm.tm_year)     /* in MM/DD/YY */
 		              , useron.logons           /* Total logons */
-		              , rows                    /* Screen length */
+		              , term->rows                    /* Screen length */
 		              , 0                       /* Highest message read */
 		              , useron.uls              /* Total files uploaded */
 		              , useron.dls);            /* Total files downloaded */
@@ -736,7 +735,7 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, uint tle
 		sys.PageBell = sys_status & SS_SYSPAGE;
 		sys.Alarm = startup->sound.answer[0] && !sound_muted(&cfg);
 		sys.ErrorCorrected = true;
-		sys.GraphicsMode = (term & NO_EXASCII) ? 'N' : 'Y';
+		sys.GraphicsMode = term->charset() == CHARSET_ASCII ? 'N' : 'Y';
 		sys.UserNetStatus = (thisnode.misc & NODE_POFF) ? 'U' : 'A'; /* Node chat status ([A]vailable or [U]navailable) */
 		SAFEPRINTF(tmp, "%u", dte_rate);
 		sys.ModemSpeed = tmp;
@@ -759,7 +758,7 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, uint tle
 		sys.MinutesLeft = (int16_t)(tleft / 60);
 		sys.NodeNum = (uint8_t)cfg.node_num;
 		sys.EventTime = "00:00";
-		sys.UseAnsi = INT_TO_BOOL(term & ANSI);
+		sys.UseAnsi = term->supports(ANSI);
 		sys.YesChar = yes_key();
 		sys.NoChar = no_key();
 		sys.Conference2 = cursubnum;
@@ -788,7 +787,7 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, uint tle
 		user.fixed.Protocol = useron.prot;
 		user.fixed.SecurityLevel = useron.level;
 		user.fixed.NumTimesOn = useron.logons;
-		user.fixed.PageLen = (uint8_t)rows;
+		user.fixed.PageLen = (uint8_t)term->rows;
 		user.fixed.NumUploads = useron.uls;
 		user.fixed.NumDownloads = useron.dls;
 		user.fixed.DailyDnldBytes = (uint32_t)logon_dlb;
@@ -853,7 +852,7 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, uint tle
 		              "%s\n%s\n%u\n%s\n%u\n%u\n%u\n%u\n%u\n%lu\n%u\n"
 		              "%" PRIu64 "\n%" PRIu64 "\n%s\n%s\n"
 		              , dropdir
-		              , (term & ANSI) ? "TRUE":"FALSE" /* ANSI ? True or False */
+		              , term->supports(ANSI) ? "TRUE":"FALSE" /* ANSI ? True or False */
 		              , useron.level            /* Security level */
 		              , useron.uls              /* Total uploads */
 		              , useron.dls              /* Total downloads */
@@ -901,9 +900,9 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, uint tle
 		 */
 		safe_snprintf(str, sizeof(str), "%s\n%d \n%d\n%u\n%u\n%u\n%u\n%s\n"
 		              , name                    // Complete name or handle of user
-		              , INT_TO_BOOL(term & ANSI) // ANSI status:  1 = yes, 0 = no, -1 = don't know
-		              , !INT_TO_BOOL(term & NO_EXASCII) // IBM Graphic characters:  1 = yes, 0 = no, -1 = unknown
-		              , rows                    // Page length of screen, in lines.  Assume 25 if unknown
+		              , term->supports(ANSI) // ANSI status:  1 = yes, 0 = no, -1 = don't know
+		              , !(term->charset() == CHARSET_ASCII) // IBM Graphic characters:  1 = yes, 0 = no, -1 = unknown
+		              , term->rows                    // Page length of screen, in lines.  Assume 25 if unknown
 		              , dte_rate                // Baud Rate:  300, 1200, 2400, 9600, 19200, etc.
 		              , online == ON_LOCAL ? 0:cfg.com_port // Com Port:  1, 2, 3, or 4.
 		              , MIN((tleft / 60), INT16_MAX) // Time Limit:  (in minutes); -1 if unknown.
@@ -931,7 +930,7 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, uint tle
 		              , useron.pass             /* User's password */
 		              , useron.level            /* User's level */
 		              , useron.misc & EXPERT ? 'Y':'N' /* Expert? */
-		              , (term & ANSI) ? 'Y':'N' /* ANSI? */
+		              , term->supports(ANSI) ? 'Y':'N' /* ANSI? */
 		              , MIN((tleft / 60), INT16_MAX) /* Minutes left */
 		              , useron.phone            /* User's phone number */
 		              , useron.location         /* User's city and state */
@@ -978,7 +977,7 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, uint tle
 		, name
 		, useron.level
 		, tleft / 60
-		, INT_TO_BOOL(term & ANSI)
+		, term->supports(ANSI)
 		, cfg.node_num);
 		lfexpand(str, misc);
 		fwrite(str, strlen(str), 1, fp);