diff --git a/src/sbbs3/ansi_terminal.cpp b/src/sbbs3/ansi_terminal.cpp
index 8ba33f793df2725d4d9994f030a029e1095d29df..d4a55f233fd3c6ca91052a8053a3431b54c491e6 100644
--- a/src/sbbs3/ansi_terminal.cpp
+++ b/src/sbbs3/ansi_terminal.cpp
@@ -65,6 +65,11 @@ const char *ANSI_Terminal::attrstr(unsigned atr)
 // Was ansi() and ansi_attr()
 char* ANSI_Terminal::attrstr(unsigned atr, unsigned curatr, char* str, size_t strsz)
 {
+	if (curatr & 0x100) {
+		if (curatr != ANSI_NORMAL)
+			sbbs->lprintf(LOG_WARNING, "Invalid current attribute %04x", curatr);
+		curatr = 0x07;
+	}
 	bool color = supports(COLOR);
 	size_t lastret;
 	if (supports(ICE_COLOR)) {
@@ -192,7 +197,7 @@ 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->putcom("\x1b[s\x1b[255B\x1b[255C\x1b[6n\x1b[u");
+		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;
@@ -210,7 +215,7 @@ bool ANSI_Terminal::getxy(unsigned* x, unsigned* y)
 	if (y != NULL)
 		*y = 0;
 
-	sbbs->putcom("\x1b[6n");  /* Request cursor position */
+	sbbs->term_out("\x1b[6n");  /* Request cursor position */
 
 	time_t start = time(NULL);
 	sbbs->sys_status &= ~SS_ABORT;
@@ -250,7 +255,7 @@ bool ANSI_Terminal::getxy(unsigned* x, unsigned* y)
 #ifdef _DEBUG
 				char dbg[128];
 				c_escape_str(str, dbg, sizeof(dbg), /* Ctrl-only? */ true);
-				lprintf(LOG_DEBUG, "Unexpected ansi_getxy response: '%s'", dbg);
+				sbbs->lprintf(LOG_DEBUG, "Unexpected ansi_getxy response: '%s'", dbg);
 #endif
 				sbbs->ungetkeys(str, /* insert */ false);
 				rsp = 0;
@@ -258,7 +263,7 @@ bool ANSI_Terminal::getxy(unsigned* x, unsigned* y)
 			}
 		}
 		if (time(NULL) - start > TIMEOUT_ANSI_GETXY) {
-			lprintf(LOG_NOTICE, "!TIMEOUT in ansi_getxy");
+			sbbs->lprintf(LOG_NOTICE, "!TIMEOUT in ansi_getxy");
 			return false;
 		}
 	}
@@ -280,58 +285,43 @@ bool ANSI_Terminal::gotoxy(unsigned x, unsigned y)
 		x = 1;
 	if (y == 0)
 		y = 1;
-	sbbs->comprintf("\x1b[%d;%dH", y, x);
-	if (x > 0)
-		column = x - 1;
-	if (y > 0)
-		row = y - 1;
-	lncntr = 0;
+	sbbs->term_printf("\x1b[%d;%dH", y, x);
 	return true;
 }
 
 // Was ansi_save
 bool ANSI_Terminal::save_cursor_pos()
 {
-	sbbs->putcom("\x1b[s");
+	sbbs->term_out("\x1b[s");
 	return true;
 }
 
 // Was ansi_restore
 bool ANSI_Terminal::restore_cursor_pos()
 {
-	sbbs->putcom("\x1b[u");
+	sbbs->term_out("\x1b[u");
 	return true;
 }
 
 void ANSI_Terminal::clearscreen()
 {
 	clear_hotspots();
-	sbbs->putcom("\x1b[2J\x1b[H");    /* clear screen, home cursor */
-	row = 0;
-	column = 0;
-	lncntr = 0;
-	lbuflen = 0;
-	lastlinelen = 0;
+	sbbs->term_out("\x1b[2J\x1b[H");    /* clear screen, home cursor */
 }
 
 void ANSI_Terminal::cleartoeos()
 {
-	sbbs->putcom("\x1b[J");
+	sbbs->term_out("\x1b[J");
 }
 
 void ANSI_Terminal::cleartoeol()
 {
-	sbbs->putcom("\x1b[K");
+	sbbs->term_out("\x1b[K");
 }
 
 void ANSI_Terminal::cursor_home()
 {
-	sbbs->putcom("\x1b[H");
-	row = 0;
-	column = 0;
-	// TODO: Did not reset lncntr
-	lncntr = 0;
-	lastlinelen = 0;
+	sbbs->term_out("\x1b[H");
 }
 
 void ANSI_Terminal::cursor_up(unsigned count = 1)
@@ -339,17 +329,9 @@ void ANSI_Terminal::cursor_up(unsigned count = 1)
 	if (count == 0)
 		return;
 	if (count > 1)
-		sbbs->comprintf("\x1b[%dA", count);
+		sbbs->term_printf("\x1b[%dA", count);
 	else
-		sbbs->putcom("\x1b[A");
-	// TODO: Old version didn't update row?
-	if (count > row)
-		count = row;
-	row -= count;
-	// TODO: Did not adjust lncntr
-	if (count > lncntr)
-		count = lncntr;
-	lncntr -= count;
+		sbbs->term_out("\x1b[A");
 }
 
 void ANSI_Terminal::cursor_down(unsigned count = 1)
@@ -357,13 +339,9 @@ void ANSI_Terminal::cursor_down(unsigned count = 1)
 	if (count == 0)
 		return;
 	if (count > 1)
-		sbbs->comprintf("\x1b[%dB", count);
+		sbbs->term_printf("\x1b[%dB", count);
 	else
-		sbbs->putcom("\x1b[B");
-	// TODO: Old version assumes this can scroll
-	if (row + count > rows)
-		count = rows - row;
-	inc_row(count);
+		sbbs->term_out("\x1b[B");
 }
 
 void ANSI_Terminal::cursor_right(unsigned count = 1)
@@ -371,26 +349,18 @@ void ANSI_Terminal::cursor_right(unsigned count = 1)
 	if (count == 0)
 		return;
 	if (count > 1)
-		sbbs->comprintf("\x1b[%dC", count);
+		sbbs->term_printf("\x1b[%dC", count);
 	else
-		sbbs->putcom("\x1b[C");
-	// TODO: Old version would move past cols
-	if (column + count > cols)
-		count = cols - column;
-	column += count;
+		sbbs->term_out("\x1b[C");
 }
 
 void ANSI_Terminal::cursor_left(unsigned count = 1) {
 	if (count == 0)
 		return;
 	if (count < 4)
-		sbbs->comprintf("%.*s", count, "\b\b\b");
-	else
-		sbbs->comprintf("\x1b[%dD", count);
-	if (column > count)
-		column -= count;
+		sbbs->term_printf("%.*s", count, "\b\b\b");
 	else
-		column = 0;
+		sbbs->term_printf("\x1b[%dD", count);
 }
 
 void ANSI_Terminal::set_output_rate(enum output_rate speed) {
@@ -413,7 +383,7 @@ void ANSI_Terminal::set_output_rate(enum output_rate speed) {
 				val = 11;
 			break;
 	}
-	sbbs->comprintf("\x1b[;%u*r", val);
+	sbbs->term_printf("\x1b[;%u*r", val);
 	cur_output_rate = speed;
 }
 
@@ -425,7 +395,7 @@ 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->putcom(str);
+	sbbs->term_out(str);
 }
 
 void ANSI_Terminal::set_mouse(unsigned flags) {
@@ -463,38 +433,229 @@ void ANSI_Terminal::set_mouse(unsigned flags) {
 	}
 }
 
-bool ANSI_Terminal::parse_outchar(char ch) {
-	// TODO: Actually parse these so the various functions don't
-	//       need to update row/column/etc.
-	if (ch == ESC && outchar_esc < ansiState_string)
+static unsigned
+get_pval(std::string params, unsigned pnum, unsigned dflt)
+{
+	try {
+		if (params == "")
+			return dflt;
+		unsigned p = 0;
+		std::string tp = 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;
+	}
+}
+
+static unsigned
+count_params(std::string params)
+{
+	std::string tp = 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;
+	}
+}
+
+// TODO: Split this up into easily understandable chunks...
+bool ANSI_Terminal::parse_outchar(char ich) {
+	unsigned char ch = static_cast<unsigned char>(ich);
+	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--;
+				if (utf8_remain)
+					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;
+				return true;
+			}
+			else if ((ch & 0xf0) == 0xe0) {
+				utf8_remain = 2;
+				if (ch == 0xF0)
+					first_continuation = true;
+				return true;
+			}
+			else if ((ch & 0xf8) == 0xf0) {
+				utf8_remain = 3;
+				if (ch == 0xF4)
+					first_continuation = true;
+				return true;
+			}
+			else
+				sbbs->lprintf(LOG_WARNING, "Sending invalid UTF-8 codepoint");
+		}
+		if (utf8_remain)
+			return true;
+	}
+
+	if (ch == ESC && outchar_esc < ansiState_string) {
 		outchar_esc = ansiState_esc;
+		ansi_was_cc = false;
+		ansi_was_string = false;
+		ansi_params = "";
+		ansi_ibs = "";
+		ansi_sequence = "";
+		ansi_final_byte = 0;
+		ansi_was_private = false;
+	}
 	else if (outchar_esc == ansiState_esc) {
-		if (ch == '[')
+		ansi_sequence += ch;
+		if (ch == '[') {
 			outchar_esc = ansiState_csi;
-		else if (ch == '_' || ch == 'P' || ch == '^' || ch == ']')
+			ansi_params = "";
+		}
+		else if (ch == '_' || ch == 'P' || ch == '^' || ch == ']') {
 			outchar_esc = ansiState_string;
-		else if (ch == 'X')
+			ansi_was_string = true;
+		}
+		else if (ch == 'X') {
 			outchar_esc = ansiState_sos;
-		else if (ch >= '@' && ch <= '_')
+			ansi_was_string = true;
+		}
+		else if (ch >= ' ' && ch <= '/') {
+			ansi_ibs += ch;
+			outchar_esc = ansiState_intermediate;
+		}
+		else if (ch >= '0' && ch <= '~') {
 			outchar_esc = ansiState_final;
-		else
+			ansi_was_cc = true;
+			ansi_final_byte = ch;
+		}
+		else {
+			// TODO: Broken sequence, position unknown
+			sbbs->lprintf(LOG_WARNING, "Sent broken ANSI sequence '%s' at %d", ansi_sequence.c_str(), __LINE__);
 			outchar_esc = ansiState_none;
+		}
 	}
 	else if (outchar_esc == ansiState_csi) {
-		if (ch >= '@' && ch <= '~')
+		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;
+			outchar_esc = ansiState_intermediate;
+		}
+		else if (ch >= '@' && ch <= '~') {
 			outchar_esc = ansiState_final;
+			ansi_final_byte = ch;
+		}
+		else {
+			// TODO: Broken sequence, position unknown
+			sbbs->lprintf(LOG_WARNING, "Sent broken ANSI sequence '%s' at %d", ansi_sequence.c_str(), __LINE__);
+			outchar_esc = ansiState_none;
+		}
+	}
+	else if (outchar_esc == ansiState_intermediate) {
+		ansi_sequence += ch;
+		if (ch >= ' ' && ch <= '/') {
+			ansi_ibs += ch;
+			outchar_esc = ansiState_intermediate;
+		}
+		else if (ch >= '0' && ch <= '~') {
+			if (!ansi_was_cc) {
+				// TODO: Broken sequence, position unknown
+				sbbs->lprintf(LOG_WARNING, "Sent broken ANSI sequence '%s' at %d", ansi_sequence.c_str(), __LINE__);
+				outchar_esc = ansiState_none;
+			}
+			else {
+				outchar_esc = ansiState_final;
+				ansi_final_byte = ch;
+			}
+		}
+		else {
+			// TODO: Broken sequence, position unknown
+			sbbs->lprintf(LOG_WARNING, "Sent broken ANSI sequence '%s' at %d", ansi_sequence.c_str(), __LINE__);
+			outchar_esc = ansiState_none;
+		}
 	}
 	else if (outchar_esc == ansiState_string) {  // APS, DCS, PM, or OSC
+		ansi_sequence += ch;
 		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
+		ansi_sequence += ch;
 		if (ch == ESC)
 			outchar_esc = ansiState_sos_esc;
 	}
 	else if (outchar_esc == ansiState_sos_esc) { // ESC inside SOS
+		ansi_sequence += ch;
 		if (ch == '\\')
 			outchar_esc = ansiState_esc;
 		else if (ch == 'X')
@@ -506,24 +667,299 @@ bool ANSI_Terminal::parse_outchar(char ch) {
 		outchar_esc = ansiState_none;
 
 	if (outchar_esc != ansiState_none) {
-		if (outchar_esc == ansiState_final)
+		if (outchar_esc == ansiState_final) {
+			if (ansi_was_cc) {
+				if (ansi_ibs == "") {
+					switch (ansi_final_byte) {
+						case 'E':	// NEL - Next Line
+							set_column();
+							inc_row();
+							break;
+						case 'M':	// RI - Reverse Line Feed
+							// TODO: line counter etc.
+							if (row)
+								row--;
+							break;
+						case 'c':	// RIS - Reset to Initial State.. homes
+							set_column();
+							set_row();
+							// TODO: line counter etc.
+							break;
+					}
+				}
+			}
+			else if (!ansi_was_string) {
+				unsigned cnt;
+				unsigned pval;
+				if (ansi_ibs == "") {
+					if (ansi_was_private) {
+						// TODO: Track things like origin mode, auto wrap, etc.
+					}
+					else {
+						switch (ansi_final_byte) {
+							case 'A':	// Cursor up
+							case 'F':	// Cursor Preceding Line
+							case 'k':	// Line Position Backward
+								// Single parameter, default 1
+								pval = get_pval(ansi_params, 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 = get_pval(ansi_params, 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 = get_pval(ansi_params, 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 = get_pval(ansi_params, 0, 1);
+								if (pval > column)
+									pval = column;
+								dec_column(pval);
+								break;
+							case 'G':	// Cursor Character Absolute
+							case '`':	// Character Position Absolute
+								pval = get_pval(ansi_params, 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 = get_pval(ansi_params, 0, 1);
+								if (pval > rows)
+									pval = rows;
+								if (pval == 0)
+									pval = 1;
+								set_row(pval - 1);
+								pval = get_pval(ansi_params, 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 = get_pval(ansi_params, 0, 1);
+								if (pval > rows)
+									pval = rows;
+								if (pval == 0)
+									pval = 1;
+								set_row(pval - 1);
+								break;
+							case 'm':	// Set Graphic Rendidtion
+								cnt = count_params(ansi_params);
+								for (unsigned i = 0; i < cnt; i++) {
+									pval = get_pval(ansi_params, i, 0);
+									switch (pval) {
+										case 0:
+											// Don't use ANSI_NORMAL, it's only for the const thing
+											curatr = 0x07;
+											break;
+										case 1:
+											curatr &= ~0x100;
+											curatr |= 0x08;
+											break;
+										case 2:
+											curatr &= ~0x108;
+											break;
+										case 5:
+										case 6:
+											curatr &= ~0x100;
+											if (flags & ICE_COLOR)
+												curatr |= BG_BRIGHT;
+											else
+												curatr |= BLINK;
+											break;
+										case 7:
+											curatr &= ~0x100;
+											if (!is_negative) {
+												curatr = (curatr & ~0x77) | ((curatr & 0x70) >> 4) | ((curatr & 0x07) << 4);
+												is_negative = true;
+											}
+											break;
+										case 8:
+											curatr &= ~0x100;
+											curatr = (curatr & ~0x07) | ((curatr & 0x70) >> 4);
+											break;
+										case 22:
+											curatr &= ~0x108;
+											break;
+										case 25:
+											curatr &= ~0x100;
+											if (flags & ICE_COLOR)
+												curatr &= ~BG_BRIGHT;
+											else
+												curatr &= ~BLINK;
+											break;
+										case 27:
+											if (is_negative) {
+												curatr = (curatr & ~0x77) | ((curatr & 0x70) >> 4) | ((curatr & 0x07) << 4);
+												is_negative = false;
+											}
+											break;
+										case 30:
+											curatr &= ~0x107;
+											curatr |= BLACK;
+											break;
+										case 31:
+											curatr &= ~0x107;
+											curatr |= RED;
+											break;
+										case 32:
+											curatr &= ~0x107;
+											curatr |= GREEN;
+											break;
+										case 33:
+											curatr &= ~0x107;
+											curatr |= BROWN;
+											break;
+										case 34:
+											curatr &= ~0x107;
+											curatr |= BLUE;
+											break;
+										case 35:
+											curatr &= ~0x107;
+											curatr |= MAGENTA;
+											break;
+										case 36:
+											curatr &= ~0x107;
+											curatr |= CYAN;
+											break;
+										case 37:
+											curatr &= ~0x107;
+											curatr |= LIGHTGRAY;
+											break;
+										case 40:
+											curatr &= ~0x370;
+											// Don't use BG_BLACK, it's only for the const thing.
+											//curatr |= BG_BLACK;
+											break;
+										case 41:
+											curatr &= ~0x370;
+											curatr |= BG_RED;
+											break;
+										case 42:
+											curatr &= ~0x370;
+											curatr |= BG_GREEN;
+											break;
+										case 43:
+											curatr &= ~0x370;
+											curatr |= BG_BROWN;
+											break;
+										case 44:
+											curatr &= ~0x370;
+											curatr |= BG_BLUE;
+											break;
+										case 45:
+											curatr &= ~0x370;
+											curatr |= BG_MAGENTA;
+											break;
+										case 46:
+											curatr &= ~0x370;
+											curatr |= BG_CYAN;
+											break;
+										case 47:
+											curatr &= ~0x370;
+											curatr |= BG_LIGHTGRAY;
+											break;
+									}
+								}
+								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;
+						}
+					}
+				}
+			}
 			outchar_esc = ansiState_none;
-		sbbs->outcom(ch);
-		return false;
+		}
 	}
-
-	if (!required_parse_outchar(ch))
-		return false;
-
-	/* Track cursor position locally */
-	switch (ch) {
-		case '\a':  // 7
-			/* Non-printing, does not go into lbuf */
-			break;
-		default:
-			// TODO: All kinds of CTRL charaters not handled properly
-			inc_column(1);
-			break;
+	else {
+		/* Track cursor position locally */
+		switch (ch) {
+			case '\a':  // 7
+				/* Non-printing */
+				break;
+			case 8:  // BS
+				dec_column();
+				break;
+			case 9:	// TAB
+				// TODO: This makes the position unknown
+				if (column < (cols - 1)) {
+					inc_column();
+					while ((column < (cols - 1)) && (column % 8))
+						inc_column();
+				}
+				break;
+			case 10: // LF
+				inc_row();
+				break;
+			case 12: // FF
+				// TODO: Technically, this makes the position unknown
+				set_row();
+				set_column();
+				break;
+			case 13: // CR
+				if (sbbs->console & CON_CR_CLREOL)
+					cleartoeol();
+				set_column();
+				break;
+			default:
+				// TODO: We'll need to handle UTF-8 here now...
+				// TODO: All kinds of CTRL charaters not handled properly
+				inc_column();
+				break;
+		}
 	}
 
 	return true;
@@ -815,16 +1251,16 @@ void ANSI_Terminal::insert_indicator() {
 	gotoxy(cols, 1);
 	int  tmpatr;
 	if (sbbs->console & CON_INSERT) {
-		sbbs->putcom(attrstr(tmpatr = BLINK | BLACK | (LIGHTGRAY << 4), curatr, str, supports(COLOR)));
-		sbbs->outcom('I');
+		sbbs->term_out(attrstr(tmpatr = BLINK | BLACK | (LIGHTGRAY << 4), curatr, str, supports(COLOR)));
+		sbbs->cp437_out('I');
 	} else {
-		sbbs->putcom(attrstr(tmpatr = ANSI_NORMAL));
-		sbbs->outcom(' ');
+		sbbs->term_out(attrstr(tmpatr = ANSI_NORMAL));
+		sbbs->cp437_out(' ');
 	}
-	sbbs->putcom(attrstr(curatr, tmpatr, str, supports(COLOR)));
+	sbbs->term_out(attrstr(curatr, tmpatr, str, supports(COLOR)));
 	restore_cursor_pos();
-	column = col;
-	this->row = row;
+	set_column(col);
+	set_row(row);
 }
 
 struct mouse_hotspot* ANSI_Terminal::add_hotspot(struct mouse_hotspot* spot) {return nullptr;}
diff --git a/src/sbbs3/ansi_terminal.h b/src/sbbs3/ansi_terminal.h
index 58af192dfd0ddf58e5fb3ccbeddad622d572d719..6cbdbb699a6a3964d0ba3d8b070d54d62427dd4d 100644
--- a/src/sbbs3/ansi_terminal.h
+++ b/src/sbbs3/ansi_terminal.h
@@ -1,12 +1,14 @@
 #ifndef ANSI_TERMINAL_H
 #define ANSI_TERMINAL_H
 
+#include <string>
 #include "terminal.h"
 
 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
@@ -53,6 +55,18 @@ public:
 
 private:
 	enum ansiState outchar_esc{ansiState_none}; // track ANSI escape seq output
+	std::string ansi_params{""};
+	std::string ansi_ibs{""};
+	std::string ansi_sequence{""};
+	bool ansi_was_cc{false};
+	bool ansi_was_string{false};
+	char ansi_final_byte{0};
+	bool ansi_was_private{false};
+	unsigned saved_row{0};
+	unsigned saved_column{0};
+	bool is_negative{0};
+	uint8_t utf8_remain{0};
+	bool first_continuation{false};
 };
 
 #endif
diff --git a/src/sbbs3/answer.cpp b/src/sbbs3/answer.cpp
index 23c9fae05687cfedb5c7d73bb1f5f286e54dfb2d..e9ab4b816098e4c8d9d3f69bd0fa05970ea330d0 100644
--- a/src/sbbs3/answer.cpp
+++ b/src/sbbs3/answer.cpp
@@ -554,7 +554,7 @@ bool sbbs_t::answer()
 			outchar(FF);
 			term->center(str);
 		} else {    /* ANSI+ terminal detection */
-			putcom( "\r\n"      /* locate cursor at column 1 */
+			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 */
diff --git a/src/sbbs3/atcodes.cpp b/src/sbbs3/atcodes.cpp
index 7b4bb421763e58a67b115c818d9ed9eb196b81ae..6b5efa31f858bad9b83683bff2a028a8c65e70b0 100644
--- a/src/sbbs3/atcodes.cpp
+++ b/src/sbbs3/atcodes.cpp
@@ -368,18 +368,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
@@ -388,36 +388,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;
 	}
 
@@ -653,7 +653,7 @@ const char* sbbs_t::atcode(const char* sp, char* str, size_t maxlen, int* pmode,
 
 	if (strcmp(sp, "PETGRFX") == 0) {
 		if (term->supports(PETSCII))
-			outcom(PETSCII_UPPERGRFX);
+			term_out(PETSCII_UPPERGRFX);
 		return nulstr;
 	}
 
diff --git a/src/sbbs3/con_hi.cpp b/src/sbbs3/con_hi.cpp
index e4663043c57fd27b6e31ed6edd9e6594737a1052..d314d16beda9ec991f621a2320007e9db9ebe544 100644
--- a/src/sbbs3/con_hi.cpp
+++ b/src/sbbs3/con_hi.cpp
@@ -37,7 +37,7 @@ void sbbs_t::redrwstr(char *strin, int i, int l, int mode)
 	if (mode)
 		bprintf(mode, "%-*.*s", l, l, strin);
 	else
-		term->column += rprintf("%-*.*s", l, l, strin);
+		rprintf("%-*.*s", l, l, strin);
 	term->cleartoeol();
 	if (i < l) {
 		auto_utf8(strin, mode);
diff --git a/src/sbbs3/con_out.cpp b/src/sbbs3/con_out.cpp
index 47cb24ea11dac6b31172fb8cf62af0002a39898b..6553c95c10bc0b8ea607d47ec3b05da0ef98e2da 100644
--- a/src/sbbs3/con_out.cpp
+++ b/src/sbbs3/con_out.cpp
@@ -118,12 +118,12 @@ int sbbs_t::bputs(const char *str, int mode)
 		}
 		if (mode & P_PETSCII) {
 			if (term->flags & PETSCII)
-				outcom(str[l++]);
+				term_out(str[l++]);
 			else
 				petscii_to_ansibbs(str[l++]);
 		} else if ((str[l] & 0x80) && (mode & P_UTF8)) {
 			if (term->flags & UTF8)
-				outcom(str[l++]);
+				term_out(str[l++]);
 			else
 				l += print_utf8_as_cp437(str + l, len - l);
 		} else
@@ -333,44 +333,160 @@ 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);
-	char utf8[UTF8_MAX_LEN + 1] = "";
-	for (l = 0; l < len && online; l++) {
-		uchar ch = str[l];
-		utf8[0] = 0;
-		if (term->flags & PETSCII) {
-			ch = cp437_to_petscii(ch);
-			if (ch == PETSCII_SOLID)
-				outcom(PETSCII_REVERSE_ON);
+	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 0x08:	// BS
+			term->cursor_left();
+			return 1;
+		case 0x09:	// TAB
+			if (term->column < (term->cols - 1)) {
+                                outchar(' ');
+                                while ((term->column < (term->cols - 1)) && (term->column % term->tabstop))
+                                        outchar(' ');
+                        }
+                        return 1;
+
+		case 0x0A:	// LF
+			term->line_feed();
+			return 1;
+		case 0x0D:	// CR
+			term->carriage_return();
+			return 1;
+		case 0x0C:	// 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->flags & UTF8) {
+		if (ch != 0x07 && ch != 0x08 && ch != 0x09 && ch != 0x0A && ch != 0x0C && ch != 0x13) {
+			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) != len)
+				return 0;
+			return 1;
 		}
-		else if ((term->flags & NO_EXASCII) && (ch & 0x80))
-			ch = exascii_to_ascii_char(ch);  /* seven bit table */
-		else if (term->flags & UTF8) {
-			enum unicode_codepoint codepoint = cp437_unicode_tbl[(uchar)ch];
-			if (codepoint != 0)
-				utf8_putc(utf8, sizeof(utf8) - 1, codepoint);
+	}
+	// PETSCII
+	else if (term->flags & PETSCII) {
+		ch = cp437_to_petscii(ch);
+		if (ch == PETSCII_SOLID) {
+			if (term_out(PETSCII_REVERSE_ON) != 1)
+				return 0;
 		}
-		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->flags & PETSCII) && ch == PETSCII_SOLID)
-				outcom(PETSCII_REVERSE_OFF);
+		if (term_out(ch) != 1)
+			return 0;
+		if (ch == PETSCII_SOLID) {
+			if (term_out(PETSCII_REVERSE_OFF) != 1)
+				return 0;
 		}
-		if (ch == '\n')
-			term->lbuflen = 0;
+		return 1;
 	}
-	return l;
+	// CP437 or US-ASCII
+	if ((term->flags & NO_EXASCII) && (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;
+	if (!term->parse_outchar(ch))
+		return 1;
+	if (term->lbuflen < LINE_BUFSIZE) {
+		if (term->lbuflen == 0)
+			term->latr = term->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 (ch == TELNET_IAC && !(telnet_mode & TELNET_MODE_OFF)) {
+		if (outcom(TELNET_IAC))
+			return 0;
+	}
+	if (outcom(ch))
+		return 0;
+	return 1;
 }
 
 /****************************************************************************/
@@ -423,9 +539,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];
@@ -434,7 +550,7 @@ 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);
+	return term_out(sbuf);
 }
 
 char* sbbs_t::term_rows(user_t* user, char* str, size_t size)
@@ -599,14 +715,18 @@ bool sbbs_t::update_nodeterm(void)
  * rputs() and unify the conversion code.  outchar() and rputs() appear
  * to be equivilent, but they're implemented differently.
  *
+ * 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.
+ * 
  * bputs  putmsg
  *   \      /
  *   outchar    rputs
  *      +---------|
- * putcom     cp437_out     outcp
- *    +-----------+-----------+
- *             term_out
- *                |
+ *            cp437_out     outcp
+ *                +-----------+
+ * putcom      term_out
+ *    +-----------+
  *              outcom
  *                |
  *           RingBufWrite
@@ -619,20 +739,22 @@ bool sbbs_t::update_nodeterm(void)
  * 
  * rputs() would effectively do nothing except call cp437_out()
  * 
- * putcom() would effectively do nothing except call term_out()
- * 
  * cp437_out() would translate from cp437 to the terminal charset
  *             this would specifically include the control characters
  *             BEL, BS, TAB, LF, FF, CR, and DEL
  * 
  * outcp() would call term_out() if UTF-8 is supported, or bputs() if not
  * 
+ * putcom() would remain a string wrapper around outcom()
+ * 
  * term_out() would update column and row, and maintain the line buffer,
  *            it would be available for a char, a uchar, a char*, and
  *            a char* + size_t
  * 
  * outcom() and RingBufWrite() would be unchanged
  * 
+ * Open questions:
+ * - Should term_out() strip/convert ANSI sequences?  Ugh, I hope not.
  */
 
 /****************************************************************************/
@@ -649,9 +771,6 @@ int sbbs_t::outchar(char ch)
 	if (console & CON_ECHO_OFF)
 		return 0;
 
-	if (!term->parse_outchar(ch))
-		return 0;
-
 	if (rainbow_index >= 0) {
 		attr(rainbow[rainbow_index]);
 		if (rainbow[rainbow_index + 1] == 0) {
@@ -660,35 +779,25 @@ int sbbs_t::outchar(char ch)
 		} else
 			++rainbow_index;
 	}
-	char utf8[UTF8_MAX_LEN + 1] = "";
-	if (!(term->flags & PETSCII)) {
-		if ((term->flags & NO_EXASCII) && (ch & 0x80))
-			ch = exascii_to_ascii_char(ch);  /* seven bit table */
-		else if (term->flags & 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];
+
+	// TODO: We may want to move these into term_out()
+	//       Otherwise they won't work with UTF-8
+	if (ch == '\n' && line_delay)
+		SLEEP(line_delay);
+
+	if (ch == FF && term->lncntr > 0 && term->row > 0) {
+		term->lncntr = 0;
+		term->newline();
+		if (!(sys_status & SS_PAUSEOFF)) {
+			pause();
+			while (term->lncntr && online && !(sys_status & SS_ABORT))
+				pause();
 		}
 	}
 
-	if ((console & CON_R_ECHOX) && (uchar)ch >= ' ') {
-		ch = *text[PasswordChar];
-	}
-	if (ch == (char)TELNET_IAC && !(telnet_mode & TELNET_MODE_OFF))
-		outcom(TELNET_IAC); /* Must escape Telnet IAC char (255) */
-	if (term->flags & 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);
-	} else {
-		if (utf8[0] != 0)
-			putcom(utf8);
-		else
-			outcom(ch);
-	}
+	cp437_out(ch);
 
 	if (term->lncntr == term->rows - 1 && ((useron.misc & (UPAUSE ^ (console & CON_PAUSEOFF))) || sys_status & SS_PAUSEON)
 	    && !(sys_status & (SS_PAUSEOFF | SS_ABORT))) {
@@ -698,14 +807,14 @@ int sbbs_t::outchar(char ch)
 	return 0;
 }
 
-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)) {
 		char str[UTF8_MAX_LEN];
 		int  len = utf8_putc(str, sizeof(str), codepoint);
 		if (len < 1)
 			return len;
-		putcom(str, len);
+		term_out(str, len);
 		term->inc_column(unicode_width(codepoint, unicode_zerowidth));
 		return 0;
 	}
@@ -714,10 +823,10 @@ 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);
+	return outcp(codepoint, str);
 }
 
 void sbbs_t::wide(const char* str)
@@ -997,8 +1106,7 @@ int sbbs_t::attr(int atr)
 	term->attrstr(atr, term->curatr, str, sizeof(str));
 	// TODO: This was rputs()
 	// TODO: We may need a raw output that goes in there
-	putcom(str);
-	term->curatr = atr;
+	term_out(str);
 	return 0;
 }
 
diff --git a/src/sbbs3/exec.cpp b/src/sbbs3/exec.cpp
index f481218bf25f1c61bf3af4f46a6f0c50bc7c9a90..196478abb84660193c4bdcbaf6ad8a53616a9c76 100644
--- a/src/sbbs3/exec.cpp
+++ b/src/sbbs3/exec.cpp
@@ -1334,7 +1334,7 @@ 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);
diff --git a/src/sbbs3/getstr.cpp b/src/sbbs3/getstr.cpp
index 1d8f90b2c08b464216b68638106adc7f5124a8bc..265cb94d614d46464d8e6a7c821911affd708447 100644
--- a/src/sbbs3/getstr.cpp
+++ b/src/sbbs3/getstr.cpp
@@ -51,9 +51,8 @@ size_t sbbs_t::getstr(char *strout, size_t maxlen, int mode, const str_list_t hi
 	if (mode & K_LINE && (term->can_highlight()) && !(mode & K_NOECHO)) {
 		attr(cfg.color[clr_inputline]);
 		for (i = 0; i < maxlen; i++)
-			outcom(' ');
+			term_out(' ');
 		term->cursor_left(maxlen);
-		term->column = org_column;
 	}
 	if (wordwrap[0]) {
 		SAFECOPY(str1, wordwrap);
@@ -92,7 +91,7 @@ size_t sbbs_t::getstr(char *strout, size_t maxlen, int mode, const str_list_t hi
 		else {
 			for (i = 0; i < l; i++)
 				outchar(BS);
-			term->column += bputs(str1, P_AUTO_UTF8);
+			bputs(str1, P_AUTO_UTF8);
 			i = l;
 		}
 		if (ch != ' ' && ch != TAB)
@@ -150,7 +149,7 @@ 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];
-					term->column += rprintf("%.*s", (int)(l - i), str1 + i);
+					rprintf("%.*s", (int)(l - i), str1 + i);
 					term->cursor_left(l - i);
 #if 0
 					if (i == maxlen - 1)
@@ -581,7 +580,7 @@ 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];
-						term->column += rprintf("%.*s", (int)(l - i), str1 + i);
+						rprintf("%.*s", (int)(l - i), str1 + i);
 						term->cursor_left(l - i);
 #if 0
 						if (i == maxlen - 1) {
@@ -596,6 +595,7 @@ size_t sbbs_t::getstr(char *strout, size_t maxlen, int mode, const str_list_t hi
 							if (i > l)
 								l = i;
 							str1[l] = 0;
+							// TODO: Test this...
 							if (utf8_str_is_valid(str1))
 								redrwstr(str1, term->column - org_column, l, P_UTF8);
 						} else {
diff --git a/src/sbbs3/main.cpp b/src/sbbs3/main.cpp
index efb5d3ff8425fc7f084fd49b6ac2c564b9232d25..cd504262194c4653bd79b656a39be35567fea2bc 100644
--- a/src/sbbs3/main.cpp
+++ b/src/sbbs3/main.cpp
@@ -1071,7 +1071,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)
@@ -4099,15 +4099,6 @@ int sbbs_t::_outcom(uchar ch)
 		return TXBOF;
 	if (!RingBufWrite(&outbuf, &ch, 1))
 		return TXBOF;
-	if (term->lbuflen < LINE_BUFSIZE) {
-		if (term->lbuflen == 0)
-			term->latr = term->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;
-	}
 	return 0;
 }
 
@@ -5585,7 +5576,7 @@ NO_SSH:
 			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->outcom(PETSCII_UPPERLOWER);
+				sbbs->term_out(PETSCII_UPPERLOWER);
 			}
 			update_terminal(sbbs);
 			// TODO: Plain text output in SSH socket
@@ -5670,7 +5661,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);
 			}
 
@@ -5697,7 +5688,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);
@@ -5759,8 +5750,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);
@@ -5835,7 +5826,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;
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/petscii_term.cpp b/src/sbbs3/petscii_term.cpp
index e46958ae5fe219a715feb008df590ea3ef35b086..94ad9fbd4a6c7bad3904c1a8418c54ff77759801 100644
--- a/src/sbbs3/petscii_term.cpp
+++ b/src/sbbs3/petscii_term.cpp
@@ -162,14 +162,11 @@ char* PETSCII_Terminal::attrstr(unsigned atr, unsigned curatr, char* str, size_t
 
 bool PETSCII_Terminal::gotoxy(unsigned x, unsigned y)
 {
-	sbbs->outcom(PETSCII_HOME);
-	row = 0;
-	column = 0;
-	lncntr = 0;
-	lastlinelen = 0;
-	lbuflen = 0;
-	cursor_down(y - 1);
-	cursor_right(x - 1);
+	sbbs->term_out(PETSCII_HOME);
+	while (row < (y - 1))
+		sbbs->term_out(PETSCII_DOWN);
+	while (column < (x - 1))
+		sbbs->term_out(PETSCII_RIGHT);
 	return true;
 }
 
@@ -196,38 +193,24 @@ void PETSCII_Terminal::carriage_return()
 void PETSCII_Terminal::line_feed(unsigned count)
 {
 	// Like cursor_down() but scrolls...
-	for (unsigned i = 0; i < count; i++) {
-		sbbs->outcom(PETSCII_DOWN);
-		inc_row();
-		lastlinelen = column;
-	}
+	for (unsigned i = 0; i < count; i++)
+		sbbs->term_out(PETSCII_DOWN);
 }
 
 void PETSCII_Terminal::backspace(unsigned int count)
 {
-	sbbs->outcom(PETSCII_DELETE);
+	sbbs->term_out(PETSCII_DELETE);
 }
 
 void PETSCII_Terminal::newline(unsigned count)
 {
-	char str[128];
-
-	sbbs->outcom('\r');
-	unsigned prevatr = curatr;
-	sbbs->putcom(attrstr(prevatr, (curatr >> 4) & 0x07, str, sizeof(str)));
-	inc_row();
-	column = 0;
+	sbbs->term_out('\r');
 }
 
 void PETSCII_Terminal::clearscreen()
 {
 	clear_hotspots();
-	sbbs->outcom('\x93');
-	row = 0;
-	column = 0;
-	lncntr = 0;
-	lastlinelen = 0;
-	lbuflen = 0;
+	sbbs->term_out('\x93');
 }
 
 void PETSCII_Terminal::cleartoeos()
@@ -247,10 +230,8 @@ void PETSCII_Terminal::cleartoeol()
 {
 	unsigned s;
 	s = column;
-	while (++s <= cols) {
-		sbbs->outcom(' ');
-		sbbs->outcom('\x14');
-	}
+	while (++s <= cols)
+		sbbs->term_out(" \x14");
 }
 
 void PETSCII_Terminal::clearline()
@@ -263,25 +244,13 @@ void PETSCII_Terminal::clearline()
 
 void PETSCII_Terminal::cursor_home()
 {
-	sbbs->outcom(PETSCII_HOME);
-	row = 0;
-	column = 0;
-	lncntr = 0;
-	lastlinelen = 0;
-	lbuflen = 0;
+	sbbs->term_out(PETSCII_HOME);
 }
 
 void PETSCII_Terminal::cursor_up(unsigned count)
 {
-	for (unsigned i = 0; i < count; i++) {
-		sbbs->outcom('\x91');
-		if (row > 0)
-			row--;
-		if (lncntr > 0)
-			lncntr--;
-		lastlinelen = column;
-		lbuflen = 0;
-	}
+	for (unsigned i = 0; i < count; i++)
+		sbbs->term_out('\x91');
 }
 
 void PETSCII_Terminal::cursor_down(unsigned count)
@@ -289,9 +258,7 @@ void PETSCII_Terminal::cursor_down(unsigned count)
 	for (unsigned i = 0; i < count; i++) {
 		if (row >= (rows - 1))
 			break;
-		sbbs->outcom(PETSCII_DOWN);
-		inc_row();
-		lastlinelen = column;
+		sbbs->term_out(PETSCII_DOWN);
 	}
 }
 
@@ -300,17 +267,16 @@ void PETSCII_Terminal::cursor_right(unsigned count)
 	for (unsigned i = 0; i < count; i++) {
 		if (column >= (cols - 1))
 			break;
-		sbbs->outcom(PETSCII_RIGHT);
-		inc_column();
+		sbbs->term_out(PETSCII_RIGHT);
 	}
 }
 
 void PETSCII_Terminal::cursor_left(unsigned count)
 {
 	for (unsigned i = 0; i < count; i++) {
-		sbbs->outcom('\x9d');
-		if (column > 0)
-			column--;
+		sbbs->term_out('\x9d');
+		if (column == 0)
+			break;
 	}
 }
 
@@ -321,8 +287,6 @@ const char* PETSCII_Terminal::type()
 
 bool PETSCII_Terminal::parse_outchar(char ch)
 {
-	if (!required_parse_outchar(ch))
-		return false;
 	switch (ch) {
 		// Zero-width characters we likely shouldn't send
 		case 0:
@@ -332,12 +296,7 @@ bool PETSCII_Terminal::parse_outchar(char ch)
 		case 4:
 		case 6:
 		case 7:
-		//case 8:  // Translated as Backspace
-		//case 9:  // Transpated as Tab
-		//case 10: // Translated as Linefeed
 		case 11:
-		//case 12: // Translated as Form Feed
-		//case 13: // Translated as Carriage Return
 		case 14:
 		case 15:
 		case 16:
@@ -363,35 +322,216 @@ bool PETSCII_Terminal::parse_outchar(char ch)
 		case '\x8F':
 			return false;
 
-		// Zero-width characters we want to pass through
-		case 5:  // White
+		// Specials that affect cursor position
+		//case 9:  // TODO: Tab or unlock case...
+		//case 10: // TODO: Linefeed or nothing
+		case '\x8D': // Shift-return
+		case 13: // Translated as Carriage Return
+			inc_row();
+			set_column(0);
+			if (curatr & 0xf0)
+				curatr = (curatr & ~0xff) | ((curatr & 0xf0) >> 4);
+			return true;
 		case 17: // Cursor down
-		case 18: // Reverse on
+			inc_row();
+			return true;
 		case 19: // Home
+			set_row();
+			set_column();
+			return true;
 		case 20: // Delete
-		case 28: // Red
+			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;
+
+		// TODO: Parse attributes
+		// Zero-width characters we want to pass through
+		case 18: // Reverse on
+			if (!reverse_on) {
+				reverse_on = true;
+				curatr = ((curatr & 0xf0) >> 4) | ((curatr & 0x0f) << 4);
+			}
+			return true;
+		case '\x92': // Reverse off
+			if (reverse_on) {
+				reverse_on = false;
+				curatr = ((curatr & 0xf0) >> 4) | ((curatr & 0x0f) << 4);
+			}
+			return true;
+		case 5:  // White
+			if (reverse_on) {
+				curatr &= ~0xF0;
+				curatr |= (WHITE << 4);
+			}
+			else {
+				curatr &= ~0x0F;
+				curatr |= WHITE;
+			}
+			return true;
+		case 28: // Red
+			if (reverse_on) {
+				curatr &= ~0xF0;
+				curatr |= (RED << 4);
+			}
+			else {
+				curatr &= ~0x0F;
+				curatr |= RED;
+			}
+			return true;
 		case 30: // Green
+			if (reverse_on) {
+				curatr &= ~0xF0;
+				curatr |= (GREEN << 4);
+			}
+			else {
+				curatr &= ~0x0F;
+				curatr |= GREEN;
+			}
+			return true;
 		case 31: // Blue
+			if (reverse_on) {
+				curatr &= ~0xF0;
+				curatr |= (BLUE << 4);
+			}
+			else {
+				curatr &= ~0x0F;
+				curatr |= BLUE;
+			}
+			return true;
 		case '\x81': // Orange
-		case '\x8D': // Shift-return
-		case '\x8E': // Upper case
+			if (reverse_on) {
+				curatr &= ~0xF0;
+				curatr |= (MAGENTA << 4);
+			}
+			else {
+				curatr &= ~0x0F;
+				curatr |= MAGENTA;
+			}
+			return true;
 		case '\x90': // Black
-		case '\x91': // Cursor up
-		case '\x92': // Reverse off
-		case '\x93': // Clear
-		case '\x94': // Insert
+			if (reverse_on) {
+				curatr &= ~0xF0;
+			}
+			else {
+				curatr &= ~0x0F;
+			}
+			return true;
 		case '\x95': // Brown
+			if (reverse_on) {
+				curatr &= ~0xF0;
+				curatr |= (BROWN << 4);
+			}
+			else {
+				curatr &= ~0x0F;
+				curatr |= BROWN;
+			}
+			return true;
 		case '\x96': // Pink
+			if (reverse_on) {
+				curatr &= ~0xF0;
+				curatr |= (LIGHTRED << 4);
+			}
+			else {
+				curatr &= ~0x0F;
+				curatr |= LIGHTRED;
+			}
+			return true;
 		case '\x97': // Dark gray
+			if (reverse_on) {
+				curatr &= ~0xF0;
+				curatr |= (DARKGRAY << 4);
+			}
+			else {
+				curatr &= ~0x0F;
+				curatr |= DARKGRAY;
+			}
+			return true;
 		case '\x98': // Gray
+			if (reverse_on) {
+				curatr &= ~0xF0;
+				curatr |= (CYAN << 4);
+			}
+			else {
+				curatr &= ~0x0F;
+				curatr |= CYAN;
+			}
+			return true;
 		case '\x99': // Light Green
+			if (reverse_on) {
+				curatr &= ~0xF0;
+				curatr |= (LIGHTGREEN << 4);
+			}
+			else {
+				curatr &= ~0x0F;
+				curatr |= LIGHTGREEN;
+			}
+			return true;
 		case '\x9A': // Light Blue
+			if (reverse_on) {
+				curatr &= ~0xF0;
+				curatr |= (LIGHTBLUE << 4);
+			}
+			else {
+				curatr &= ~0x0F;
+				curatr |= LIGHTBLUE;
+			}
+			return true;
 		case '\x9B': // Light Gray
+			if (reverse_on) {
+				curatr &= ~0xF0;
+				curatr |= (LIGHTGRAY << 4);
+			}
+			else {
+				curatr &= ~0x0F;
+				curatr |= LIGHTGRAY;
+			}
+			return true;
 		case '\x9C': // Purple
-		case '\x9D': // Cursor Left
+			if (reverse_on) {
+				curatr &= ~0xF0;
+				curatr |= (LIGHTMAGENTA << 4);
+			}
+			else {
+				curatr &= ~0x0F;
+				curatr |= LIGHTMAGENTA;
+			}
+			return true;
 		case '\x9E': // Yellow
+			if (reverse_on) {
+				curatr &= ~0xF0;
+				curatr |= (YELLOW << 4);
+			}
+			else {
+				curatr &= ~0x0F;
+				curatr |= YELLOW;
+			}
+			return true;
 		case '\x9F': // Cyan
+			if (reverse_on) {
+				curatr &= ~0xF0;
+				curatr |= (LIGHTCYAN << 4);
+			}
+			else {
+				curatr &= ~0x0F;
+				curatr |= LIGHTCYAN;
+			}
+			return true;
+		case '\x8E': // Upper case
+		case '\x93': // Clear
+		case '\x94': // Insert
 			return true;
 
 		// Everything else is assumed one byte wide
@@ -468,10 +608,10 @@ void PETSCII_Terminal::insert_indicator()
 	gotoxy(cols, 1);
 	if (sbbs->console & CON_INSERT) {
 		sbbs->attr(BLINK | BLACK | (LIGHTGRAY << 4));
-		sbbs->outchar('I');
+		sbbs->term_out('I');
 	} else {
 		sbbs->attr(ANSI_NORMAL);
-		sbbs->outchar(' ');
+		sbbs->term_out(' ');
 	}
 	sbbs->attr(oldatr);
 	gotoxy(x, y);
diff --git a/src/sbbs3/petscii_term.h b/src/sbbs3/petscii_term.h
index e3bc664f5f7d7f8a8e78f0fe926c56f53b20931c..b1c0eea7c66bee820607de6dfcaf78a173bd68c4 100644
--- a/src/sbbs3/petscii_term.h
+++ b/src/sbbs3/petscii_term.h
@@ -7,6 +7,7 @@ class PETSCII_Terminal : public Terminal {
 private:
 	unsigned saved_x{0};
 	unsigned saved_y{0};
+	bool reverse_on{false};
 
 public:
 
diff --git a/src/sbbs3/prntfile.cpp b/src/sbbs3/prntfile.cpp
index 0c4a479ef30e19410de86c06e3351cf373c79792..94d6f743e8a8be36fcb202cbf00f053b2499eb2a 100644
--- a/src/sbbs3/prntfile.cpp
+++ b/src/sbbs3/prntfile.cpp
@@ -150,7 +150,7 @@ bool sbbs_t::printfile(const char* fname, int mode, int org_cols, JSObject* obj)
 		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 */
diff --git a/src/sbbs3/putmsg.cpp b/src/sbbs3/putmsg.cpp
index d9ab734dfc10ce8b83bfc4d8b7fe2a5fd5e42614..92feebc61f518f917a22af3aafff9eb473c8a5e4 100644
--- a/src/sbbs3/putmsg.cpp
+++ b/src/sbbs3/putmsg.cpp
@@ -62,7 +62,7 @@ char sbbs_t::putmsg(const char *buf, int mode, int org_cols, JSObject* obj)
 		term->set_output_rate(output_rate);
 
 	if (mode & P_PETSCII)
-		outcom(PETSCII_UPPERLOWER);
+		term_out(PETSCII_UPPERLOWER);
 
 	attr_sp = 0;  /* clear any saved attributes */
 
@@ -499,57 +499,12 @@ char sbbs_t::putmsgfrag(const char* buf, int& mode, unsigned org_cols, JSObject*
 			size_t skip = sizeof(char);
 			if (mode & P_PETSCII) {
 				if (term->flags & PETSCII) {
-					outcom(str[l]);
-					// TODO: Do this in outcom()
-					//       Splitting it out here is bad.
-					//       It's very similar to what parse_outchar() does.
-					switch ((uchar)str[l]) {
-						case '\r':  // PETSCII "Return" / new-line
-							term->column = 0;
-						/* fall-through */
-						case PETSCII_DOWN:
-							term->lncntr++;
-							term->inc_row();
-							break;
-						case PETSCII_CLEAR:
-						case PETSCII_HOME:
-							term->row = 0;
-							term->column = 0;
-							term->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:
-							term->inc_column();
-							break;
-					}
+					term_out(str[l]);
 				} else
 					petscii_to_ansibbs(str[l]);
 			} else if ((str[l] & 0x80) && (mode & P_UTF8)) {
 				if (term->flags & UTF8)
-					outcom(str[l]);
+					term_out(str[l]);
 				else
 					skip = print_utf8_as_cp437(str + l, len - l);
 			} else {
diff --git a/src/sbbs3/sbbs.h b/src/sbbs3/sbbs.h
index 435ab96bcdd31865909930aefe3bdec0766acd0b..2725db4c02340f6a014eb2ef88eea22c77a67550 100644
--- a/src/sbbs3/sbbs.h
+++ b/src/sbbs3/sbbs.h
@@ -592,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);
@@ -875,7 +875,7 @@ public:
 	/* con_out.cpp */
 	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
@@ -892,14 +892,14 @@ 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
 	;
 	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);
+	int		outcp(enum unicode_codepoint, char cp437_fallback);
+	int		outcp(enum unicode_codepoint, const char* cp437_fallback = NULL);
 	void	wide(const char*);
 	// TODO: There appear to describe the USER, not the terminal...
 	char*	term_rows(user_t*, char* str, size_t);
@@ -918,6 +918,10 @@ public:
 	char*	auto_utf8(const char*, int& mode);
 	// TODO: Not To ANSI_Terminal
 	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;
diff --git a/src/sbbs3/terminal.cpp b/src/sbbs3/terminal.cpp
index 5f4ae560e3ee2d619faf2c4f45340d7c9649004a..1f4b7dc9b0df35609de57e7a49a6b20308b74bfe 100644
--- a/src/sbbs3/terminal.cpp
+++ b/src/sbbs3/terminal.cpp
@@ -3,47 +3,6 @@
 #include "petscii_term.h"
 #include "link_list.h"
 
-/*
- * Returns true if the caller should send the char, false if
- * this function handled it (ie: via outcom(), or stripping it)
- */
-bool Terminal::required_parse_outchar(char ch) {
-	switch (ch) {
-		// Special values
-		case 8:  // BS
-			cursor_left();
-			return false;
-		case 9:
-			// TODO: Original would wrap, this one (hopefully) doesn't.
-			//       Further, use outchar() instead of outcom() to get
-			//       the spaces into the line buffer instead of tabs
-			if (column < (cols - 1)) {
-				sbbs->outchar(' ');
-				while ((column < (cols - 1)) && (column % tabstop))
-					sbbs->outchar(' ');
-			}
-			return false;
-		case 10: // LF
-			// Terminates lbuf
-			if (sbbs->line_delay)
-				SLEEP(sbbs->line_delay);
-			line_feed();
-			return false;
-		case 12: // FF
-			// Does not go into lbuf
-			check_clear_pause();
-			clearscreen();
-			return false;
-		case 13: // CR
-			if (sbbs->console & CON_CR_CLREOL)
-				cleartoeol();
-			carriage_return();
-			return false;
-		// Everything else is assumed one byte wide
-	}
-	return true;
-}
-
 void Terminal::clear_hotspots(void)
 {
 	if (!(flags & MOUSE))
@@ -51,7 +10,7 @@ void Terminal::clear_hotspots(void)
 	int spots = listCountNodes(mouse_hotspots);
 	if (spots) {
 #if 0 //def _DEBUG
-		lprintf(LOG_DEBUG, "Clearing %ld mouse hot spots", spots);
+		sbbs->lprintf(LOG_DEBUG, "Clearing %ld mouse hot spots", spots);
 #endif
 		listFreeNodes(mouse_hotspots);
 		if (!(sbbs->console & CON_MOUSE_SCROLL))
@@ -74,7 +33,7 @@ void Terminal::scroll_hotspots(unsigned count)
 	}
 #ifdef _DEBUG
 	if (spots)
-		lprintf(LOG_DEBUG, "Scrolled %u mouse hot-spots %u rows (%u remain)", spots, count, remain);
+		sbbs->lprintf(LOG_DEBUG, "Scrolled %u mouse hot-spots %u rows (%u remain)", spots, count, remain);
 #endif
 	if (remain < 1)
 		clear_hotspots();
@@ -198,6 +157,50 @@ void Terminal::inc_column(unsigned count) {
 	}
 }
 
+void Terminal::dec_row(unsigned count) {
+	if (column)
+		lastlinelen = column;
+	// TODO: Never allow dec_row to scroll up
+	if (count > row)
+		count = row;
+#if 0
+	// TODO: 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;
+	lbuflen = 0;
+}
+
+void Terminal::dec_column(unsigned count) {
+	// TODO: Never allow dec_column() to wrap
+	if (count > column)
+		count = column;
+	column -= count;
+	if (column == 0)
+		lbuflen = 0;
+}
+
+void Terminal::set_row(unsigned val) {
+	if (val >= rows)
+		val = rows - 1;
+	if (column)
+		lastlinelen = column;
+	row = val;
+	lncntr = 0;
+	lbuflen = 0;
+}
+
+void Terminal::set_column(unsigned val) {
+	if (val >= cols)
+		val = cols - 1;
+	column = val;
+}
+
 void Terminal::cond_newline() {
 	if (column > 0)
 		newline();
@@ -245,19 +248,6 @@ list_node_t *Terminal::find_hotspot(unsigned x, unsigned y)
 	return node;
 }
 
-void Terminal::check_clear_pause()
-{
-	if (lncntr > 0 && row > 0) {
-		lncntr = 0;
-		newline();
-		if (!(sbbs->sys_status & SS_PAUSEOFF)) {
-			sbbs->pause();
-			while (lncntr && sbbs->online && !(sbbs->sys_status & SS_ABORT))
-				sbbs->pause();
-		}
-	}
-}
-
 static void
 flags_fixup(uint32_t& flags)
 {
diff --git a/src/sbbs3/terminal.h b/src/sbbs3/terminal.h
index d5d5bd50e8b8ba81ffcf52ffc630d4309b8beb28..c954abf8b36807619455fad364ac44c118c03414 100644
--- a/src/sbbs3/terminal.h
+++ b/src/sbbs3/terminal.h
@@ -181,34 +181,28 @@ public:
 	}
 
 	virtual void carriage_return() {
-		lastlinelen = column;
-		sbbs->outcom('\r');
-		column = 0;
+		sbbs->term_out('\r');
+		set_column();
 	}
 
 	virtual void line_feed(unsigned count = 1) {
 		for (unsigned i = 0; i < count; i++)
-			sbbs->outcom('\n');
+			sbbs->term_out('\n');
 		inc_row(count);
-		lbuflen = 0;
 	}
 
 	/*
 	 * Destructive backspace.
 	 * TODO: This seems to be the only one of this family that
 	 *       checks CON_ECHO_OFF itself.  Figure out why and either
-	 *       remove it, or add it to all the rest that call outcom()
+	 *       remove it, or add it to all the rest that call term_out()
 	 */
 	virtual void backspace(unsigned int count = 1) {
 		if (sbbs->console & CON_ECHO_OFF)
 			return;
 		for (unsigned i = 0; i < count; i++) {
-			if (column > 0) {
+			if (column > 0)
 				sbbs->putcom("\b \b");
-				column--;
-				if (lbuflen)
-					lbuflen--;
-			}
 			else
 				break;
 		}
@@ -224,14 +218,10 @@ public:
 	}
 
 	virtual void clearscreen() {
-		check_clear_pause();
 		clear_hotspots();
-		sbbs->outcom(FF);
-		row = 0;
-		column = 0;
-		lncntr = 0;
-		lbuflen = 0;
-		lastlinelen = 0;
+		sbbs->term_out(FF);
+		set_row();
+		set_column();
 	}
 
 	virtual void cleartoeos() {}
@@ -250,7 +240,7 @@ public:
 	virtual void cursor_right(unsigned count = 1) {
 		for (unsigned i = 0; i < count; i++) {
 			if (column < (cols - 1))
-				sbbs->outcom(' ');
+				sbbs->term_out(' ');
 			else
 				break;
 		}
@@ -258,10 +248,8 @@ public:
 
 	virtual void cursor_left(unsigned count = 1) {
 		for (unsigned i = 0; i < count; i++) {
-			if (column > 0) {
-				sbbs->outcom('\b');
-				column--;
-			}
+			if (column > 0)
+				sbbs->term_out('\b');
 			else
 				break;
 		}
@@ -324,14 +312,9 @@ public:
 
 	/*
 	 * Returns true if the caller should send the char, false if
-	 * this function handled it (ie: via outcom(), or stripping it)
+	 * this function handled it (ie: via term_out(), or stripping it)
 	 */
 	virtual bool parse_outchar(char ch) {
-		if (!lbuflen)
-			latr = curatr;
-		if (!required_parse_outchar(ch))
-			return false;
-
 		switch (ch) {
 			// Zero-width characters we likely shouldn't send
 			case 0:  // NUL
@@ -355,7 +338,6 @@ public:
 			case 24: // CAN
 			case 25: // EM
 			case 26: // SUB
-			case 27: // ESC - This one is especially troubling
 			case 28: // FS
 			case 29: // GS
 			case 30: // RS
@@ -363,14 +345,40 @@ public:
 				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
 			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
+				if (sbbs->console & CON_CR_CLREOL)
+					cleartoeol();
+				set_column();
+				return true;
+
 			// Everything else is assumed one byte wide
 			default:
-				if (lbuflen < LINE_BUFSIZE)
-					lbuf[lbuflen++] = ch;
 				inc_column();
 				return true;
 		}
@@ -411,15 +419,17 @@ public:
 		if (line == NULL)
 			return false;
 		lbuflen = 0;
+		// 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 outcom() loop
+		// Switch from rputs to term_out()
 		// This way we don't need to re-encode
-		for (unsigned u = 0; line->buf[u]; u++)
-			sbbs->outcom(line->buf[u]);
+		sbbs->term_out(line->buf);
 		curatr = line->end_attr;
-		column = line->column;
 		free(line);
-		insert_indicator();
 		return true;
 	}
 
@@ -439,8 +449,6 @@ public:
 		return true;
 	}
 
-	bool required_parse_outchar(char ch);
-
 	void clear_hotspots(void);
 	void scroll_hotspots(unsigned count);
 
@@ -452,12 +460,15 @@ public:
 	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);
 	list_node_t *find_hotspot(unsigned x, unsigned y);
-	void check_clear_pause();
 };
 
 void update_terminal(sbbs_t *sbbsptr);
diff --git a/src/sbbs3/useredit.cpp b/src/sbbs3/useredit.cpp
index b9dc08aa5f1412eec2b5d016bf72dfbeb78475ed..f51964c22175e8deaef38ce8171952aadc6114a5 100644
--- a/src/sbbs3/useredit.cpp
+++ b/src/sbbs3/useredit.cpp
@@ -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