diff --git a/exec/load/syncterm_cache.js b/exec/load/syncterm_cache.js
index bdc03ea8d33d5d79589c1c9f795b2e3e5a206bd7..b89d29caf1c4f9db45ab6e411afac7b63993c396 100644
--- a/exec/load/syncterm_cache.js
+++ b/exec/load/syncterm_cache.js
@@ -1,3 +1,5 @@
+require("sbbsdefs.js", "K_NUL");
+
 function SyncTERMCache() {
 	this.supported = this.supports_syncterm_cache();
 };
@@ -14,7 +16,7 @@ SyncTERMCache.prototype.read_apc = function (timeout)
 	if (timeout === undefined)
 		timeout = 1000;
 
-	while((ch = console.inkey(timeout)) !== '') {
+	while((ch = console.inkey(K_NUL, timeout)) !== null) {
 		switch(state) {
 			case 0:
 				if (ch == '\x1b') {
diff --git a/exec/textedit.js b/exec/textedit.js
index b268712d207a06a1a12c7fc5082eaec2a5231113..31e03760af99ca296885c0dd2e0490591b67936d 100644
--- a/exec/textedit.js
+++ b/exec/textedit.js
@@ -11,6 +11,7 @@ var msglens = [];
 var displaywith = 0;
 var inprow = (console.screen_columns < 80) ? 18 : 17;
 var ch;
+var modified = {};
 
 // TODO: This should be in a separate (JSON) file...
 var details = {
@@ -139,8 +140,6 @@ console.cleartoeos = function(attr)
 
 function format_entry(str)
 {
-	// bbs.command_str = '@';
-	// .replace(/@/g, "@U+40:@@")
 	return str.replace(/[\x00-\x1F\x80-\x9F\\]/g, function(match) {
 		switch(match) {
 			case '\n':
@@ -345,8 +344,7 @@ function get_tvals() {
 	for (i in tvals) {
 		tnames[tvals[i]] = i;
 	}
-	// TODO: Why is this one higher than the last?
-	last_entry = tvals.TOTAL_TEXT - 1;
+	last_entry = tvals.TOTAL_TEXT;
 }
 
 function newmsg(updpos)
@@ -408,12 +406,43 @@ function get_msgnum()
 	console.attributes = 7;
 }
 
+function track_mod()
+{
+	if (modified[msg] === undefined) {
+		modified[msg] = {original: bbs.text(msg), modified: true};
+	}
+}
+
+function check_undone()
+{
+	if (modified[msg] !== undefined && msgstr === modified[msg].original) {
+		delete modified[msg];
+	}
+}
+
+function load_saved()
+{
+	var sfile = new File("textedit.ini");
+	if (!sfile.open('r'))
+		return;
+	var keys = sfile.iniGetKeys();
+	var key;
+	var str;
+	for (key in keys) {
+		str = sfile.iniGetValue(null, keys[key]);
+		bbs.replace_text(keys[key], str);
+	}
+	sfile.close();
+}
+
 get_tvals();
 newmsg();
+load_saved();
 var done = false;
 var skip_redraw = false;
 var forcectrl = false;
 var tmp;
+var sfile;
 while (!done) {
 	if (!skip_redraw)
 		redraw(msgstr, msg);
@@ -465,15 +494,27 @@ while (!done) {
 			break;
 		case ctrl('Z'):
 			bbs.revert_text(msg);
+			if (modified[msg] !== undefined)
+				delete modified[msg];
 			newmsg();
 			break;
 		case ctrl('Q'):
 			done = true;
 			break;
+		case ctrl('S'):
+			sfile = new File("textedit.ini");
+			if (!sfile.open(sfile.exists ? 'r+':'w+'))
+				break;
+			for (tmp in modified)
+				sfile.iniSetValue(null, tnames[tmp], bbs.text(tmp));
+			sfile.close();
+			break;
 		case '\b':
 			if (pos) {
+				track_mod();
 				msgstr = msgstr.slice(0, pos - 1) + msgstr.slice(pos);
 				bbs.replace_text(msg, msgstr);
+				check_undone();
 				pos--;
 				newmsg(false);
 			}
@@ -544,8 +585,10 @@ while (!done) {
 		default:
 			if (ch < ' ' && !forcectrl)
 				break;
+			track_mod();
 			msgstr = msgstr.slice(0, pos) + ch + msgstr.slice(pos);
 			bbs.replace_text(msg, msgstr);
+			check_undone();
 			pos++;
 			newmsg(false);
 			break;
diff --git a/src/sbbs3/con_out.cpp b/src/sbbs3/con_out.cpp
index eda850dfd58735ff572b3578b1b69face90ef4a9..a219d293a3c353299bb4bfd4c35b5fd9291fff32 100644
--- a/src/sbbs3/con_out.cpp
+++ b/src/sbbs3/con_out.cpp
@@ -100,8 +100,7 @@ int sbbs_t::bputs(const char *str, int mode)
 		if (!(mode & P_NOATCODES) && str[l] == '@') {
 			if (str == mnestr          /* Mnemonic string or */
 			    || (mode & P_ATCODES) /* trusted parameters to formatted string */
-			    || (str >= text[0]    /* Straight out of TEXT.DAT */
-			        && str <= text[TOTAL_TEXT - 1])) {
+				) {
 				i = show_atcode(str + l);   /* return 0 if not valid @ code */
 				l += i;                   /* i is length of code string */
 				if (i)                   /* if valid string, go to top */
diff --git a/src/sbbs3/textgen.c b/src/sbbs3/textgen.c
index d1429de81bd575267c64bd6a0527280d67d9dc19..981b7bcb0068069666cd53fc24995c00a79a60ba 100644
--- a/src/sbbs3/textgen.c
+++ b/src/sbbs3/textgen.c
@@ -269,7 +269,7 @@ int main(int argc, char **argv)
 	fputs("#endif\n", text_h);
 	fclose(text_h);
 	fputs("\n", text_js);
-	fprintf(text_js, "var TOTAL_TEXT=%d;\n", i);
+	fprintf(text_js, "var TOTAL_TEXT=%d;\n", i - 1);
 	fprintf(text_js, "\nthis;\n");
 	fclose(text_js);
 	fputs("};\n", text_defaults_c);