diff --git a/exec/load/attr_conv.js b/exec/load/attr_conv.js
new file mode 100644
index 0000000000000000000000000000000000000000..e3630db5fb74f45894bb6756ea0253d245c15e85
--- /dev/null
+++ b/exec/load/attr_conv.js
@@ -0,0 +1,1132 @@
+// This file specifies some functions for converting attribute codes from
+// other systems to Synchronet attribute codes.  The functions all take a
+// string and return a string:
+//
+// function WWIVAttrsToSyncAttrs(pText)
+// function PCBoardAttrsToSyncAttrs(pText)
+// function wildcatAttrsToSyncAttrs(pText)
+// function celerityAttrsToSyncAttrs(pText)
+// function renegadeAttrsToSyncAttrs(pText)
+// function ANSIAttrsToSyncAttrs(pText)
+// function convertAttrsToSyncPerSysCfg(pText)
+//
+// Author: Eric Oulashin (AKA Nightfox)
+// BBS: Digital Distortion
+// BBS address: digitaldistortionbbs.com (or digdist.synchro.net)
+
+
+// JS strict mode says octal literals (such as "\1") are deprecated, but
+// it seems better to use \1 than using an actual control character in
+// a text file
+//"use strict";
+
+require("sbbsdefs.js", "SYS_RENEGADE");
+
+/////////////////////////////////////////////////////////////////////////
+// Functions for converting other BBS color codes to Synchronet attribute codes
+
+// Converts WWIV attribute codes to Synchronet attribute codes.
+//
+// Parameters:
+//  pText: A string containing the text to convert
+//
+// Return value: The text with the color codes converted
+function WWIVAttrsToSyncAttrs(pText)
+{
+	// First, see if the text has any WWIV-style attribute codes at
+	// all.  We'll be performing a bunch of search & replace commands,
+	// so we don't want to do all that work for nothing.. :)
+	if (/\x03[0-9]/.test(pText))
+	{
+		var text = pText.replace(/\x030/g, "\1n");			// Normal
+		text = text.replace(/\x031/g, "\1n\1c\1h");			// Bright cyan
+		text = text.replace(/\x032/g, "\1n\1y\1h");			// Bright yellow
+		text = text.replace(/\x033/g, "\1n\1m");			// Magenta
+		text = text.replace(/\x034/g, "\1n\1h\1w\1" + "4");	// Bright white on blue
+		text = text.replace(/\x035/g, "\1n\1g");			// Green
+		text = text.replace(/\x036/g, "\1h\1r\1i");			// Bright red, blinking
+		text = text.replace(/\x037/g, "\1n\1h\1b");			// Bright blue
+		text = text.replace(/\x038/g, "\1n\1b");			// Blue
+		text = text.replace(/\x039/g, "\1n\1c");			// Cyan
+		return text;
+	}
+	else
+		return pText; // No WWIV-style color attribute found, so just return the text.
+}
+
+// Converts PCBoard attribute codes to Synchronet attribute codes.
+//
+// Parameters:
+//  pText: A string containing the text to convert
+//
+// Return value: The text with the color codes converted
+function PCBoardAttrsToSyncAttrs(pText)
+{
+	// First, see if the text has any PCBoard-style attribute codes at
+	// all.  We'll be performing a bunch of search & replace commands,
+	// so we don't want to do all that work for nothing.. :)
+	if (/@[xX][0-9A-Fa-f]{2}/.test(pText))
+	{
+		// Black background
+		var text = pText.replace(/@[xX]00/g, "\1n\1k\1" + "0"); // Black on black
+		text = text.replace(/@[xX]01/g, "\1n\1b\1" + "0"); // Blue on black
+		text = text.replace(/@[xX]02/g, "\1n\1g\1" + "0"); // Green on black
+		text = text.replace(/@[xX]03/g, "\1n\1c\1" + "0"); // Cyan on black
+		text = text.replace(/@[xX]04/g, "\1n\1r\1" + "0"); // Red on black
+		text = text.replace(/@[xX]05/g, "\1n\1m\1" + "0"); // Magenta on black
+		text = text.replace(/@[xX]06/g, "\1n\1y\1" + "0"); // Yellow/brown on black
+		text = text.replace(/@[xX]07/g, "\1n\1w\1" + "0"); // White on black
+		text = text.replace(/@[xX]08/g, "\1n\1w\1" + "0"); // White on black
+		text = text.replace(/@[xX]09/g, "\1n\1w\1" + "0"); // White on black
+		text = text.replace(/@[xX]08/g, "\1h\1k\1" + "0"); // Bright black on black
+		text = text.replace(/@[xX]09/g, "\1h\1b\1" + "0"); // Bright blue on black
+		text = text.replace(/@[xX]0[Aa]/g, "\1h\1g\1" + "0"); // Bright green on black
+		text = text.replace(/@[xX]0[Bb]/g, "\1h\1c\1" + "0"); // Bright cyan on black
+		text = text.replace(/@[xX]0[Cc]/g, "\1h\1r\1" + "0"); // Bright red on black
+		text = text.replace(/@[xX]0[Dd]/g, "\1h\1m\1" + "0"); // Bright magenta on black
+		text = text.replace(/@[xX]0[Ee]/g, "\1h\1y\1" + "0"); // Bright yellow on black
+		text = text.replace(/@[xX]0[Ff]/g, "\1h\1w\1" + "0"); // Bright white on black
+		// Blinking foreground
+
+		// Blue background
+		text = text.replace(/@[xX]10/g, "\1n\1k\1" + "4"); // Black on blue
+		text = text.replace(/@[xX]11/g, "\1n\1b\1" + "4"); // Blue on blue
+		text = text.replace(/@[xX]12/g, "\1n\1g\1" + "4"); // Green on blue
+		text = text.replace(/@[xX]13/g, "\1n\1c\1" + "4"); // Cyan on blue
+		text = text.replace(/@[xX]14/g, "\1n\1r\1" + "4"); // Red on blue
+		text = text.replace(/@[xX]15/g, "\1n\1m\1" + "4"); // Magenta on blue
+		text = text.replace(/@[xX]16/g, "\1n\1y\1" + "4"); // Yellow/brown on blue
+		text = text.replace(/@[xX]17/g, "\1n\1w\1" + "4"); // White on blue
+		text = text.replace(/@[xX]18/g, "\1h\1k\1" + "4"); // Bright black on blue
+		text = text.replace(/@[xX]19/g, "\1h\1b\1" + "4"); // Bright blue on blue
+		text = text.replace(/@[xX]1[Aa]/g, "\1h\1g\1" + "4"); // Bright green on blue
+		text = text.replace(/@[xX]1[Bb]/g, "\1h\1c\1" + "4"); // Bright cyan on blue
+		text = text.replace(/@[xX]1[Cc]/g, "\1h\1r\1" + "4"); // Bright red on blue
+		text = text.replace(/@[xX]1[Dd]/g, "\1h\1m\1" + "4"); // Bright magenta on blue
+		text = text.replace(/@[xX]1[Ee]/g, "\1h\1y\1" + "4"); // Bright yellow on blue
+		text = text.replace(/@[xX]1[Ff]/g, "\1h\1w\1" + "4"); // Bright white on blue
+
+		// Green background
+		text = text.replace(/@[xX]20/g, "\1n\1k\1" + "2"); // Black on green
+		text = text.replace(/@[xX]21/g, "\1n\1b\1" + "2"); // Blue on green
+		text = text.replace(/@[xX]22/g, "\1n\1g\1" + "2"); // Green on green
+		text = text.replace(/@[xX]23/g, "\1n\1c\1" + "2"); // Cyan on green
+		text = text.replace(/@[xX]24/g, "\1n\1r\1" + "2"); // Red on green
+		text = text.replace(/@[xX]25/g, "\1n\1m\1" + "2"); // Magenta on green
+		text = text.replace(/@[xX]26/g, "\1n\1y\1" + "2"); // Yellow/brown on green
+		text = text.replace(/@[xX]27/g, "\1n\1w\1" + "2"); // White on green
+		text = text.replace(/@[xX]28/g, "\1h\1k\1" + "2"); // Bright black on green
+		text = text.replace(/@[xX]29/g, "\1h\1b\1" + "2"); // Bright blue on green
+		text = text.replace(/@[xX]2[Aa]/g, "\1h\1g\1" + "2"); // Bright green on green
+		text = text.replace(/@[xX]2[Bb]/g, "\1h\1c\1" + "2"); // Bright cyan on green
+		text = text.replace(/@[xX]2[Cc]/g, "\1h\1r\1" + "2"); // Bright red on green
+		text = text.replace(/@[xX]2[Dd]/g, "\1h\1m\1" + "2"); // Bright magenta on green
+		text = text.replace(/@[xX]2[Ee]/g, "\1h\1y\1" + "2"); // Bright yellow on green
+		text = text.replace(/@[xX]2[Ff]/g, "\1h\1w\1" + "2"); // Bright white on green
+
+		// Cyan background
+		text = text.replace(/@[xX]30/g, "\1n\1k\1" + "6"); // Black on cyan
+		text = text.replace(/@[xX]31/g, "\1n\1b\1" + "6"); // Blue on cyan
+		text = text.replace(/@[xX]32/g, "\1n\1g\1" + "6"); // Green on cyan
+		text = text.replace(/@[xX]33/g, "\1n\1c\1" + "6"); // Cyan on cyan
+		text = text.replace(/@[xX]34/g, "\1n\1r\1" + "6"); // Red on cyan
+		text = text.replace(/@[xX]35/g, "\1n\1m\1" + "6"); // Magenta on cyan
+		text = text.replace(/@[xX]36/g, "\1n\1y\1" + "6"); // Yellow/brown on cyan
+		text = text.replace(/@[xX]37/g, "\1n\1w\1" + "6"); // White on cyan
+		text = text.replace(/@[xX]38/g, "\1h\1k\1" + "6"); // Bright black on cyan
+		text = text.replace(/@[xX]39/g, "\1h\1b\1" + "6"); // Bright blue on cyan
+		text = text.replace(/@[xX]3[Aa]/g, "\1h\1g\1" + "6"); // Bright green on cyan
+		text = text.replace(/@[xX]3[Bb]/g, "\1h\1c\1" + "6"); // Bright cyan on cyan
+		text = text.replace(/@[xX]3[Cc]/g, "\1h\1r\1" + "6"); // Bright red on cyan
+		text = text.replace(/@[xX]3[Dd]/g, "\1h\1m\1" + "6"); // Bright magenta on cyan
+		text = text.replace(/@[xX]3[Ee]/g, "\1h\1y\1" + "6"); // Bright yellow on cyan
+		text = text.replace(/@[xX]3[Ff]/g, "\1h\1w\1" + "6"); // Bright white on cyan
+
+		// Red background
+		text = text.replace(/@[xX]40/g, "\1n\1k\1" + "1"); // Black on red
+		text = text.replace(/@[xX]41/g, "\1n\1b\1" + "1"); // Blue on red
+		text = text.replace(/@[xX]42/g, "\1n\1g\1" + "1"); // Green on red
+		text = text.replace(/@[xX]43/g, "\1n\1c\1" + "1"); // Cyan on red
+		text = text.replace(/@[xX]44/g, "\1n\1r\1" + "1"); // Red on red
+		text = text.replace(/@[xX]45/g, "\1n\1m\1" + "1"); // Magenta on red
+		text = text.replace(/@[xX]46/g, "\1n\1y\1" + "1"); // Yellow/brown on red
+		text = text.replace(/@[xX]47/g, "\1n\1w\1" + "1"); // White on red
+		text = text.replace(/@[xX]48/g, "\1h\1k\1" + "1"); // Bright black on red
+		text = text.replace(/@[xX]49/g, "\1h\1b\1" + "1"); // Bright blue on red
+		text = text.replace(/@[xX]4[Aa]/g, "\1h\1g\1" + "1"); // Bright green on red
+		text = text.replace(/@[xX]4[Bb]/g, "\1h\1c\1" + "1"); // Bright cyan on red
+		text = text.replace(/@[xX]4[Cc]/g, "\1h\1r\1" + "1"); // Bright red on red
+		text = text.replace(/@[xX]4[Dd]/g, "\1h\1m\1" + "1"); // Bright magenta on red
+		text = text.replace(/@[xX]4[Ee]/g, "\1h\1y\1" + "1"); // Bright yellow on red
+		text = text.replace(/@[xX]4[Ff]/g, "\1h\1w\1" + "1"); // Bright white on red
+
+		// Magenta background
+		text = text.replace(/@[xX]50/g, "\1n\1k\1" + "5"); // Black on magenta
+		text = text.replace(/@[xX]51/g, "\1n\1b\1" + "5"); // Blue on magenta
+		text = text.replace(/@[xX]52/g, "\1n\1g\1" + "5"); // Green on magenta
+		text = text.replace(/@[xX]53/g, "\1n\1c\1" + "5"); // Cyan on magenta
+		text = text.replace(/@[xX]54/g, "\1n\1r\1" + "5"); // Red on magenta
+		text = text.replace(/@[xX]55/g, "\1n\1m\1" + "5"); // Magenta on magenta
+		text = text.replace(/@[xX]56/g, "\1n\1y\1" + "5"); // Yellow/brown on magenta
+		text = text.replace(/@[xX]57/g, "\1n\1w\1" + "5"); // White on magenta
+		text = text.replace(/@[xX]58/g, "\1h\1k\1" + "5"); // Bright black on magenta
+		text = text.replace(/@[xX]59/g, "\1h\1b\1" + "5"); // Bright blue on magenta
+		text = text.replace(/@[xX]5[Aa]/g, "\1h\1g\1" + "5"); // Bright green on magenta
+		text = text.replace(/@[xX]5[Bb]/g, "\1h\1c\1" + "5"); // Bright cyan on magenta
+		text = text.replace(/@[xX]5[Cc]/g, "\1h\1r\1" + "5"); // Bright red on magenta
+		text = text.replace(/@[xX]5[Dd]/g, "\1h\1m\1" + "5"); // Bright magenta on magenta
+		text = text.replace(/@[xX]5[Ee]/g, "\1h\1y\1" + "5"); // Bright yellow on magenta
+		text = text.replace(/@[xX]5[Ff]/g, "\1h\1w\1" + "5"); // Bright white on magenta
+
+		// Brown background
+		text = text.replace(/@[xX]60/g, "\1n\1k\1" + "3"); // Black on brown
+		text = text.replace(/@[xX]61/g, "\1n\1b\1" + "3"); // Blue on brown
+		text = text.replace(/@[xX]62/g, "\1n\1g\1" + "3"); // Green on brown
+		text = text.replace(/@[xX]63/g, "\1n\1c\1" + "3"); // Cyan on brown
+		text = text.replace(/@[xX]64/g, "\1n\1r\1" + "3"); // Red on brown
+		text = text.replace(/@[xX]65/g, "\1n\1m\1" + "3"); // Magenta on brown
+		text = text.replace(/@[xX]66/g, "\1n\1y\1" + "3"); // Yellow/brown on brown
+		text = text.replace(/@[xX]67/g, "\1n\1w\1" + "3"); // White on brown
+		text = text.replace(/@[xX]68/g, "\1h\1k\1" + "3"); // Bright black on brown
+		text = text.replace(/@[xX]69/g, "\1h\1b\1" + "3"); // Bright blue on brown
+		text = text.replace(/@[xX]6[Aa]/g, "\1h\1g\1" + "3"); // Bright breen on brown
+		text = text.replace(/@[xX]6[Bb]/g, "\1h\1c\1" + "3"); // Bright cyan on brown
+		text = text.replace(/@[xX]6[Cc]/g, "\1h\1r\1" + "3"); // Bright red on brown
+		text = text.replace(/@[xX]6[Dd]/g, "\1h\1m\1" + "3"); // Bright magenta on brown
+		text = text.replace(/@[xX]6[Ee]/g, "\1h\1y\1" + "3"); // Bright yellow on brown
+		text = text.replace(/@[xX]6[Ff]/g, "\1h\1w\1" + "3"); // Bright white on brown
+
+		// White background
+		text = text.replace(/@[xX]70/g, "\1n\1k\1" + "7"); // Black on white
+		text = text.replace(/@[xX]71/g, "\1n\1b\1" + "7"); // Blue on white
+		text = text.replace(/@[xX]72/g, "\1n\1g\1" + "7"); // Green on white
+		text = text.replace(/@[xX]73/g, "\1n\1c\1" + "7"); // Cyan on white
+		text = text.replace(/@[xX]74/g, "\1n\1r\1" + "7"); // Red on white
+		text = text.replace(/@[xX]75/g, "\1n\1m\1" + "7"); // Magenta on white
+		text = text.replace(/@[xX]76/g, "\1n\1y\1" + "7"); // Yellow/brown on white
+		text = text.replace(/@[xX]77/g, "\1n\1w\1" + "7"); // White on white
+		text = text.replace(/@[xX]78/g, "\1h\1k\1" + "7"); // Bright black on white
+		text = text.replace(/@[xX]79/g, "\1h\1b\1" + "7"); // Bright blue on white
+		text = text.replace(/@[xX]7[Aa]/g, "\1h\1g\1" + "7"); // Bright green on white
+		text = text.replace(/@[xX]7[Bb]/g, "\1h\1c\1" + "7"); // Bright cyan on white
+		text = text.replace(/@[xX]7[Cc]/g, "\1h\1r\1" + "7"); // Bright red on white
+		text = text.replace(/@[xX]7[Dd]/g, "\1h\1m\1" + "7"); // Bright magenta on white
+		text = text.replace(/@[xX]7[Ee]/g, "\1h\1y\1" + "7"); // Bright yellow on white
+		text = text.replace(/@[xX]7[Ff]/g, "\1h\1w\1" + "7"); // Bright white on white
+
+		// Black background, blinking foreground
+		text = text.replace(/@[xX]80/g, "\1n\1k\1" + "0\1i"); // Blinking black on black
+		text = text.replace(/@[xX]81/g, "\1n\1b\1" + "0\1i"); // Blinking blue on black
+		text = text.replace(/@[xX]82/g, "\1n\1g\1" + "0\1i"); // Blinking green on black
+		text = text.replace(/@[xX]83/g, "\1n\1c\1" + "0\1i"); // Blinking cyan on black
+		text = text.replace(/@[xX]84/g, "\1n\1r\1" + "0\1i"); // Blinking red on black
+		text = text.replace(/@[xX]85/g, "\1n\1m\1" + "0\1i"); // Blinking magenta on black
+		text = text.replace(/@[xX]86/g, "\1n\1y\1" + "0\1i"); // Blinking yellow/brown on black
+		text = text.replace(/@[xX]87/g, "\1n\1w\1" + "0\1i"); // Blinking white on black
+		text = text.replace(/@[xX]88/g, "\1h\1k\1" + "0\1i"); // Blinking bright black on black
+		text = text.replace(/@[xX]89/g, "\1h\1b\1" + "0\1i"); // Blinking bright blue on black
+		text = text.replace(/@[xX]8[Aa]/g, "\1h\1g\1" + "0\1i"); // Blinking bright green on black
+		text = text.replace(/@[xX]8[Bb]/g, "\1h\1c\1" + "0\1i"); // Blinking bright cyan on black
+		text = text.replace(/@[xX]8[Cc]/g, "\1h\1r\1" + "0\1i"); // Blinking bright red on black
+		text = text.replace(/@[xX]8[Dd]/g, "\1h\1m\1" + "0\1i"); // Blinking bright magenta on black
+		text = text.replace(/@[xX]8[Ee]/g, "\1h\1y\1" + "0\1i"); // Blinking bright yellow on black
+		text = text.replace(/@[xX]8[Ff]/g, "\1h\1w\1" + "0\1i"); // Blinking bright white on black
+
+		// Blue background, blinking foreground
+		text = text.replace(/@[xX]90/g, "\1n\1k\1" + "4\1i"); // Blinking black on blue
+		text = text.replace(/@[xX]91/g, "\1n\1b\1" + "4\1i"); // Blinking blue on blue
+		text = text.replace(/@[xX]92/g, "\1n\1g\1" + "4\1i"); // Blinking green on blue
+		text = text.replace(/@[xX]93/g, "\1n\1c\1" + "4\1i"); // Blinking cyan on blue
+		text = text.replace(/@[xX]94/g, "\1n\1r\1" + "4\1i"); // Blinking red on blue
+		text = text.replace(/@[xX]95/g, "\1n\1m\1" + "4\1i"); // Blinking magenta on blue
+		text = text.replace(/@[xX]96/g, "\1n\1y\1" + "4\1i"); // Blinking yellow/brown on blue
+		text = text.replace(/@[xX]97/g, "\1n\1w\1" + "4\1i"); // Blinking white on blue
+		text = text.replace(/@[xX]98/g, "\1h\1k\1" + "4\1i"); // Blinking bright black on blue
+		text = text.replace(/@[xX]99/g, "\1h\1b\1" + "4\1i"); // Blinking bright blue on blue
+		text = text.replace(/@[xX]9[Aa]/g, "\1h\1g\1" + "4\1i"); // Blinking bright green on blue
+		text = text.replace(/@[xX]9[Bb]/g, "\1h\1c\1" + "4\1i"); // Blinking bright cyan on blue
+		text = text.replace(/@[xX]9[Cc]/g, "\1h\1r\1" + "4\1i"); // Blinking bright red on blue
+		text = text.replace(/@[xX]9[Dd]/g, "\1h\1m\1" + "4\1i"); // Blinking bright magenta on blue
+		text = text.replace(/@[xX]9[Ee]/g, "\1h\1y\1" + "4\1i"); // Blinking bright yellow on blue
+		text = text.replace(/@[xX]9[Ff]/g, "\1h\1w\1" + "4\1i"); // Blinking bright white on blue
+
+		// Green background, blinking foreground
+		text = text.replace(/@[xX][Aa]0/g, "\1n\1k\1" + "2\1i"); // Blinking black on green
+		text = text.replace(/@[xX][Aa]1/g, "\1n\1b\1" + "2\1i"); // Blinking blue on green
+		text = text.replace(/@[xX][Aa]2/g, "\1n\1g\1" + "2\1i"); // Blinking green on green
+		text = text.replace(/@[xX][Aa]3/g, "\1n\1c\1" + "2\1i"); // Blinking cyan on green
+		text = text.replace(/@[xX][Aa]4/g, "\1n\1r\1" + "2\1i"); // Blinking red on green
+		text = text.replace(/@[xX][Aa]5/g, "\1n\1m\1" + "2\1i"); // Blinking magenta on green
+		text = text.replace(/@[xX][Aa]6/g, "\1n\1y\1" + "2\1i"); // Blinking yellow/brown on green
+		text = text.replace(/@[xX][Aa]7/g, "\1n\1w\1" + "2\1i"); // Blinking white on green
+		text = text.replace(/@[xX][Aa]8/g, "\1h\1k\1" + "2\1i"); // Blinking bright black on green
+		text = text.replace(/@[xX][Aa]9/g, "\1h\1b\1" + "2\1i"); // Blinking bright blue on green
+		text = text.replace(/@[xX][Aa][Aa]/g, "\1h\1g\1" + "2\1i"); // Blinking bright green on green
+		text = text.replace(/@[xX][Aa][Bb]/g, "\1h\1c\1" + "2\1i"); // Blinking bright cyan on green
+		text = text.replace(/@[xX][Aa][Cc]/g, "\1h\1r\1" + "2\1i"); // Blinking bright red on green
+		text = text.replace(/@[xX][Aa][Dd]/g, "\1h\1m\1" + "2\1i"); // Blinking bright magenta on green
+		text = text.replace(/@[xX][Aa][Ee]/g, "\1h\1y\1" + "2\1i"); // Blinking bright yellow on green
+		text = text.replace(/@[xX][Aa][Ff]/g, "\1h\1w\1" + "2\1i"); // Blinking bright white on green
+
+		// Cyan background, blinking foreground
+		text = text.replace(/@[xX][Bb]0/g, "\1n\1k\1" + "6\1i"); // Blinking black on cyan
+		text = text.replace(/@[xX][Bb]1/g, "\1n\1b\1" + "6\1i"); // Blinking blue on cyan
+		text = text.replace(/@[xX][Bb]2/g, "\1n\1g\1" + "6\1i"); // Blinking green on cyan
+		text = text.replace(/@[xX][Bb]3/g, "\1n\1c\1" + "6\1i"); // Blinking cyan on cyan
+		text = text.replace(/@[xX][Bb]4/g, "\1n\1r\1" + "6\1i"); // Blinking red on cyan
+		text = text.replace(/@[xX][Bb]5/g, "\1n\1m\1" + "6\1i"); // Blinking magenta on cyan
+		text = text.replace(/@[xX][Bb]6/g, "\1n\1y\1" + "6\1i"); // Blinking yellow/brown on cyan
+		text = text.replace(/@[xX][Bb]7/g, "\1n\1w\1" + "6\1i"); // Blinking white on cyan
+		text = text.replace(/@[xX][Bb]8/g, "\1h\1k\1" + "6\1i"); // Blinking bright black on cyan
+		text = text.replace(/@[xX][Bb]9/g, "\1h\1b\1" + "6\1i"); // Blinking bright blue on cyan
+		text = text.replace(/@[xX][Bb][Aa]/g, "\1h\1g\1" + "6\1i"); // Blinking bright green on cyan
+		text = text.replace(/@[xX][Bb][Bb]/g, "\1h\1c\1" + "6\1i"); // Blinking bright cyan on cyan
+		text = text.replace(/@[xX][Bb][Cc]/g, "\1h\1r\1" + "6\1i"); // Blinking bright red on cyan
+		text = text.replace(/@[xX][Bb][Dd]/g, "\1h\1m\1" + "6\1i"); // Blinking bright magenta on cyan
+		text = text.replace(/@[xX][Bb][Ee]/g, "\1h\1y\1" + "6\1i"); // Blinking bright yellow on cyan
+		text = text.replace(/@[xX][Bb][Ff]/g, "\1h\1w\1" + "6\1i"); // Blinking bright white on cyan
+
+		// Red background, blinking foreground
+		text = text.replace(/@[xX][Cc]0/g, "\1n\1k\1" + "1\1i"); // Blinking black on red
+		text = text.replace(/@[xX][Cc]1/g, "\1n\1b\1" + "1\1i"); // Blinking blue on red
+		text = text.replace(/@[xX][Cc]2/g, "\1n\1g\1" + "1\1i"); // Blinking green on red
+		text = text.replace(/@[xX][Cc]3/g, "\1n\1c\1" + "1\1i"); // Blinking cyan on red
+		text = text.replace(/@[xX][Cc]4/g, "\1n\1r\1" + "1\1i"); // Blinking red on red
+		text = text.replace(/@[xX][Cc]5/g, "\1n\1m\1" + "1\1i"); // Blinking magenta on red
+		text = text.replace(/@[xX][Cc]6/g, "\1n\1y\1" + "1\1i"); // Blinking yellow/brown on red
+		text = text.replace(/@[xX][Cc]7/g, "\1n\1w\1" + "1\1i"); // Blinking white on red
+		text = text.replace(/@[xX][Cc]8/g, "\1h\1k\1" + "1\1i"); // Blinking bright black on red
+		text = text.replace(/@[xX][Cc]9/g, "\1h\1b\1" + "1\1i"); // Blinking bright blue on red
+		text = text.replace(/@[xX][Cc][Aa]/g, "\1h\1g\1" + "1\1i"); // Blinking bright green on red
+		text = text.replace(/@[xX][Cc][Bb]/g, "\1h\1c\1" + "1\1i"); // Blinking bright cyan on red
+		text = text.replace(/@[xX][Cc][Cc]/g, "\1h\1r\1" + "1\1i"); // Blinking bright red on red
+		text = text.replace(/@[xX][Cc][Dd]/g, "\1h\1m\1" + "1\1i"); // Blinking bright magenta on red
+		text = text.replace(/@[xX][Cc][Ee]/g, "\1h\1y\1" + "1\1i"); // Blinking bright yellow on red
+		text = text.replace(/@[xX][Cc][Ff]/g, "\1h\1w\1" + "1\1i"); // Blinking bright white on red
+
+		// Magenta background, blinking foreground
+		text = text.replace(/@[xX][Dd]0/g, "\1n\1k\1" + "5\1i"); // Blinking black on magenta
+		text = text.replace(/@[xX][Dd]1/g, "\1n\1b\1" + "5\1i"); // Blinking blue on magenta
+		text = text.replace(/@[xX][Dd]2/g, "\1n\1g\1" + "5\1i"); // Blinking green on magenta
+		text = text.replace(/@[xX][Dd]3/g, "\1n\1c\1" + "5\1i"); // Blinking cyan on magenta
+		text = text.replace(/@[xX][Dd]4/g, "\1n\1r\1" + "5\1i"); // Blinking red on magenta
+		text = text.replace(/@[xX][Dd]5/g, "\1n\1m\1" + "5\1i"); // Blinking magenta on magenta
+		text = text.replace(/@[xX][Dd]6/g, "\1n\1y\1" + "5\1i"); // Blinking yellow/brown on magenta
+		text = text.replace(/@[xX][Dd]7/g, "\1n\1w\1" + "5\1i"); // Blinking white on magenta
+		text = text.replace(/@[xX][Dd]8/g, "\1h\1k\1" + "5\1i"); // Blinking bright black on magenta
+		text = text.replace(/@[xX][Dd]9/g, "\1h\1b\1" + "5\1i"); // Blinking bright blue on magenta
+		text = text.replace(/@[xX][Dd][Aa]/g, "\1h\1g\1" + "5\1i"); // Blinking bright green on magenta
+		text = text.replace(/@[xX][Dd][Bb]/g, "\1h\1c\1" + "5\1i"); // Blinking bright cyan on magenta
+		text = text.replace(/@[xX][Dd][Cc]/g, "\1h\1r\1" + "5\1i"); // Blinking bright red on magenta
+		text = text.replace(/@[xX][Dd][Dd]/g, "\1h\1m\1" + "5\1i"); // Blinking bright magenta on magenta
+		text = text.replace(/@[xX][Dd][Ee]/g, "\1h\1y\1" + "5\1i"); // Blinking bright yellow on magenta
+		text = text.replace(/@[xX][Dd][Ff]/g, "\1h\1w\1" + "5\1i"); // Blinking bright white on magenta
+
+		// Brown background, blinking foreground
+		text = text.replace(/@[xX][Ee]0/g, "\1n\1k\1" + "3\1i"); // Blinking black on brown
+		text = text.replace(/@[xX][Ee]1/g, "\1n\1b\1" + "3\1i"); // Blinking blue on brown
+		text = text.replace(/@[xX][Ee]2/g, "\1n\1g\1" + "3\1i"); // Blinking green on brown
+		text = text.replace(/@[xX][Ee]3/g, "\1n\1c\1" + "3\1i"); // Blinking cyan on brown
+		text = text.replace(/@[xX][Ee]4/g, "\1n\1r\1" + "3\1i"); // Blinking red on brown
+		text = text.replace(/@[xX][Ee]5/g, "\1n\1m\1" + "3\1i"); // Blinking magenta on brown
+		text = text.replace(/@[xX][Ee]6/g, "\1n\1y\1" + "3\1i"); // Blinking yellow/brown on brown
+		text = text.replace(/@[xX][Ee]7/g, "\1n\1w\1" + "3\1i"); // Blinking white on brown
+		text = text.replace(/@[xX][Ee]8/g, "\1h\1k\1" + "3\1i"); // Blinking bright black on brown
+		text = text.replace(/@[xX][Ee]9/g, "\1h\1b\1" + "3\1i"); // Blinking bright blue on brown
+		text = text.replace(/@[xX][Ee][Aa]/g, "\1h\1g\1" + "3\1i"); // Blinking bright green on brown
+		text = text.replace(/@[xX][Ee][Bb]/g, "\1h\1c\1" + "3\1i"); // Blinking bright cyan on brown
+		text = text.replace(/@[xX][Ee][Cc]/g, "\1h\1r\1" + "3\1i"); // Blinking bright red on brown
+		text = text.replace(/@[xX][Ee][Dd]/g, "\1h\1m\1" + "3\1i"); // Blinking bright magenta on brown
+		text = text.replace(/@[xX][Ee][Ee]/g, "\1h\1y\1" + "3\1i"); // Blinking bright yellow on brown
+		text = text.replace(/@[xX][Ee][Ff]/g, "\1h\1w\1" + "3\1i"); // Blinking bright white on brown
+
+		// White background, blinking foreground
+		text = text.replace(/@[xX][Ff]0/g, "\1n\1k\1" + "7\1i"); // Blinking black on white
+		text = text.replace(/@[xX][Ff]1/g, "\1n\1b\1" + "7\1i"); // Blinking blue on white
+		text = text.replace(/@[xX][Ff]2/g, "\1n\1g\1" + "7\1i"); // Blinking green on white
+		text = text.replace(/@[xX][Ff]3/g, "\1n\1c\1" + "7\1i"); // Blinking cyan on white
+		text = text.replace(/@[xX][Ff]4/g, "\1n\1r\1" + "7\1i"); // Blinking red on white
+		text = text.replace(/@[xX][Ff]5/g, "\1n\1m\1" + "7\1i"); // Blinking magenta on white
+		text = text.replace(/@[xX][Ff]6/g, "\1n\1y\1" + "7\1i"); // Blinking yellow/brown on white
+		text = text.replace(/@[xX][Ff]7/g, "\1n\1w\1" + "7\1i"); // Blinking white on white
+		text = text.replace(/@[xX][Ff]8/g, "\1h\1k\1" + "7\1i"); // Blinking bright black on white
+		text = text.replace(/@[xX][Ff]9/g, "\1h\1b\1" + "7\1i"); // Blinking bright blue on white
+		text = text.replace(/@[xX][Ff][Aa]/g, "\1h\1g\1" + "7\1i"); // Blinking bright green on white
+		text = text.replace(/@[xX][Ff][Bb]/g, "\1h\1c\1" + "7\1i"); // Blinking bright cyan on white
+		text = text.replace(/@[xX][Ff][Cc]/g, "\1h\1r\1" + "7\1i"); // Blinking bright red on white
+		text = text.replace(/@[xX][Ff][Dd]/g, "\1h\1m\1" + "7\1i"); // Blinking bright magenta on white
+		text = text.replace(/@[xX][Ff][Ee]/g, "\1h\1y\1" + "7\1i"); // Blinking bright yellow on white
+		text = text.replace(/@[xX][Ff][Ff]/g, "\1h\1w\1" + "7\1i"); // Blinking bright white on white
+
+		return text;
+	}
+	else
+		return pText; // No PCBoard-style attribute codes found, so just return the text.
+}
+
+// Converts Wildcat attribute codes to Synchronet attribute codes.
+//
+// Parameters:
+//  pText: A string containing the text to convert
+//
+// Return value: The text with the color codes converted
+function wildcatAttrsToSyncAttrs(pText)
+{
+	// First, see if the text has any Wildcat-style attribute codes at
+	// all.  We'll be performing a bunch of search & replace commands,
+	// so we don't want to do all that work for nothing.. :)
+	if (/@[0-9A-Fa-f]{2}@/.test(pText))
+	{
+		// Black background
+		var text = pText.replace(/@00@/g, "\1n\1k\1" + "0"); // Black on black
+		text = text.replace(/@01@/g, "\1n\1b\1" + "0"); // Blue on black
+		text = text.replace(/@02@/g, "\1n\1g\1" + "0"); // Green on black
+		text = text.replace(/@03@/g, "\1n\1c\1" + "0"); // Cyan on black
+		text = text.replace(/@04@/g, "\1n\1r\1" + "0"); // Red on black
+		text = text.replace(/@05@/g, "\1n\1m\1" + "0"); // Magenta on black
+		text = text.replace(/@06@/g, "\1n\1y\1" + "0"); // Yellow/brown on black
+		text = text.replace(/@07@/g, "\1n\1w\1" + "0"); // White on black
+		text = text.replace(/@08@/g, "\1n\1w\1" + "0"); // White on black
+		text = text.replace(/@09@/g, "\1n\1w\1" + "0"); // White on black
+		text = text.replace(/@08@/g, "\1h\1k\1" + "0"); // Bright black on black
+		text = text.replace(/@09@/g, "\1h\1b\1" + "0"); // Bright blue on black
+		text = text.replace(/@0[Aa]@/g, "\1h\1g\1" + "0"); // Bright green on black
+		text = text.replace(/@0[Bb]@/g, "\1h\1c\1" + "0"); // Bright cyan on black
+		text = text.replace(/@0[Cc]@/g, "\1h\1r\1" + "0"); // Bright red on black
+		text = text.replace(/@0[Dd]@/g, "\1h\1m\1" + "0"); // Bright magenta on black
+		text = text.replace(/@0[Ee]@/g, "\1h\1y\1" + "0"); // Bright yellow on black
+		text = text.replace(/@0[Ff]@/g, "\1h\1w\1" + "0"); // Bright white on black
+		// Blinking foreground
+
+		// Blue background
+		text = text.replace(/@10@/g, "\1n\1k\1" + "4"); // Black on blue
+		text = text.replace(/@11@/g, "\1n\1b\1" + "4"); // Blue on blue
+		text = text.replace(/@12@/g, "\1n\1g\1" + "4"); // Green on blue
+		text = text.replace(/@13@/g, "\1n\1c\1" + "4"); // Cyan on blue
+		text = text.replace(/@14@/g, "\1n\1r\1" + "4"); // Red on blue
+		text = text.replace(/@15@/g, "\1n\1m\1" + "4"); // Magenta on blue
+		text = text.replace(/@16@/g, "\1n\1y\1" + "4"); // Yellow/brown on blue
+		text = text.replace(/@17@/g, "\1n\1w\1" + "4"); // White on blue
+		text = text.replace(/@18@/g, "\1h\1k\1" + "4"); // Bright black on blue
+		text = text.replace(/@19@/g, "\1h\1b\1" + "4"); // Bright blue on blue
+		text = text.replace(/@1[Aa]@/g, "\1h\1g\1" + "4"); // Bright green on blue
+		text = text.replace(/@1[Bb]@/g, "\1h\1c\1" + "4"); // Bright cyan on blue
+		text = text.replace(/@1[Cc]@/g, "\1h\1r\1" + "4"); // Bright red on blue
+		text = text.replace(/@1[Dd]@/g, "\1h\1m\1" + "4"); // Bright magenta on blue
+		text = text.replace(/@1[Ee]@/g, "\1h\1y\1" + "4"); // Bright yellow on blue
+		text = text.replace(/@1[Ff]@/g, "\1h\1w\1" + "4"); // Bright white on blue
+
+		// Green background
+		text = text.replace(/@20@/g, "\1n\1k\1" + "2"); // Black on green
+		text = text.replace(/@21@/g, "\1n\1b\1" + "2"); // Blue on green
+		text = text.replace(/@22@/g, "\1n\1g\1" + "2"); // Green on green
+		text = text.replace(/@23@/g, "\1n\1c\1" + "2"); // Cyan on green
+		text = text.replace(/@24@/g, "\1n\1r\1" + "2"); // Red on green
+		text = text.replace(/@25@/g, "\1n\1m\1" + "2"); // Magenta on green
+		text = text.replace(/@26@/g, "\1n\1y\1" + "2"); // Yellow/brown on green
+		text = text.replace(/@27@/g, "\1n\1w\1" + "2"); // White on green
+		text = text.replace(/@28@/g, "\1h\1k\1" + "2"); // Bright black on green
+		text = text.replace(/@29@/g, "\1h\1b\1" + "2"); // Bright blue on green
+		text = text.replace(/@2[Aa]@/g, "\1h\1g\1" + "2"); // Bright green on green
+		text = text.replace(/@2[Bb]@/g, "\1h\1c\1" + "2"); // Bright cyan on green
+		text = text.replace(/@2[Cc]@/g, "\1h\1r\1" + "2"); // Bright red on green
+		text = text.replace(/@2[Dd]@/g, "\1h\1m\1" + "2"); // Bright magenta on green
+		text = text.replace(/@2[Ee]@/g, "\1h\1y\1" + "2"); // Bright yellow on green
+		text = text.replace(/@2[Ff]@/g, "\1h\1w\1" + "2"); // Bright white on green
+
+		// Cyan background
+		text = text.replace(/@30@/g, "\1n\1k\1" + "6"); // Black on cyan
+		text = text.replace(/@31@/g, "\1n\1b\1" + "6"); // Blue on cyan
+		text = text.replace(/@32@/g, "\1n\1g\1" + "6"); // Green on cyan
+		text = text.replace(/@33@/g, "\1n\1c\1" + "6"); // Cyan on cyan
+		text = text.replace(/@34@/g, "\1n\1r\1" + "6"); // Red on cyan
+		text = text.replace(/@35@/g, "\1n\1m\1" + "6"); // Magenta on cyan
+		text = text.replace(/@36@/g, "\1n\1y\1" + "6"); // Yellow/brown on cyan
+		text = text.replace(/@37@/g, "\1n\1w\1" + "6"); // White on cyan
+		text = text.replace(/@38@/g, "\1h\1k\1" + "6"); // Bright black on cyan
+		text = text.replace(/@39@/g, "\1h\1b\1" + "6"); // Bright blue on cyan
+		text = text.replace(/@3[Aa]@/g, "\1h\1g\1" + "6"); // Bright green on cyan
+		text = text.replace(/@3[Bb]@/g, "\1h\1c\1" + "6"); // Bright cyan on cyan
+		text = text.replace(/@3[Cc]@/g, "\1h\1r\1" + "6"); // Bright red on cyan
+		text = text.replace(/@3[Dd]@/g, "\1h\1m\1" + "6"); // Bright magenta on cyan
+		text = text.replace(/@3[Ee]@/g, "\1h\1y\1" + "6"); // Bright yellow on cyan
+		text = text.replace(/@3[Ff]@/g, "\1h\1w\1" + "6"); // Bright white on cyan
+
+		// Red background
+		text = text.replace(/@40@/g, "\1n\1k\1" + "1"); // Black on red
+		text = text.replace(/@41@/g, "\1n\1b\1" + "1"); // Blue on red
+		text = text.replace(/@42@/g, "\1n\1g\1" + "1"); // Green on red
+		text = text.replace(/@43@/g, "\1n\1c\1" + "1"); // Cyan on red
+		text = text.replace(/@44@/g, "\1n\1r\1" + "1"); // Red on red
+		text = text.replace(/@45@/g, "\1n\1m\1" + "1"); // Magenta on red
+		text = text.replace(/@46@/g, "\1n\1y\1" + "1"); // Yellow/brown on red
+		text = text.replace(/@47@/g, "\1n\1w\1" + "1"); // White on red
+		text = text.replace(/@48@/g, "\1h\1k\1" + "1"); // Bright black on red
+		text = text.replace(/@49@/g, "\1h\1b\1" + "1"); // Bright blue on red
+		text = text.replace(/@4[Aa]@/g, "\1h\1g\1" + "1"); // Bright green on red
+		text = text.replace(/@4[Bb]@/g, "\1h\1c\1" + "1"); // Bright cyan on red
+		text = text.replace(/@4[Cc]@/g, "\1h\1r\1" + "1"); // Bright red on red
+		text = text.replace(/@4[Dd]@/g, "\1h\1m\1" + "1"); // Bright magenta on red
+		text = text.replace(/@4[Ee]@/g, "\1h\1y\1" + "1"); // Bright yellow on red
+		text = text.replace(/@4[Ff]@/g, "\1h\1w\1" + "1"); // Bright white on red
+
+		// Magenta background
+		text = text.replace(/@50@/g, "\1n\1k\1" + "5"); // Black on magenta
+		text = text.replace(/@51@/g, "\1n\1b\1" + "5"); // Blue on magenta
+		text = text.replace(/@52@/g, "\1n\1g\1" + "5"); // Green on magenta
+		text = text.replace(/@53@/g, "\1n\1c\1" + "5"); // Cyan on magenta
+		text = text.replace(/@54@/g, "\1n\1r\1" + "5"); // Red on magenta
+		text = text.replace(/@55@/g, "\1n\1m\1" + "5"); // Magenta on magenta
+		text = text.replace(/@56@/g, "\1n\1y\1" + "5"); // Yellow/brown on magenta
+		text = text.replace(/@57@/g, "\1n\1w\1" + "5"); // White on magenta
+		text = text.replace(/@58@/g, "\1h\1k\1" + "5"); // Bright black on magenta
+		text = text.replace(/@59@/g, "\1h\1b\1" + "5"); // Bright blue on magenta
+		text = text.replace(/@5[Aa]@/g, "\1h\1g\1" + "5"); // Bright green on magenta
+		text = text.replace(/@5[Bb]@/g, "\1h\1c\1" + "5"); // Bright cyan on magenta
+		text = text.replace(/@5[Cc]@/g, "\1h\1r\1" + "5"); // Bright red on magenta
+		text = text.replace(/@5[Dd]@/g, "\1h\1m\1" + "5"); // Bright magenta on magenta
+		text = text.replace(/@5[Ee]@/g, "\1h\1y\1" + "5"); // Bright yellow on magenta
+		text = text.replace(/@5[Ff]@/g, "\1h\1w\1" + "5"); // Bright white on magenta
+
+		// Brown background
+		text = text.replace(/@60@/g, "\1n\1k\1" + "3"); // Black on brown
+		text = text.replace(/@61@/g, "\1n\1b\1" + "3"); // Blue on brown
+		text = text.replace(/@62@/g, "\1n\1g\1" + "3"); // Green on brown
+		text = text.replace(/@63@/g, "\1n\1c\1" + "3"); // Cyan on brown
+		text = text.replace(/@64@/g, "\1n\1r\1" + "3"); // Red on brown
+		text = text.replace(/@65@/g, "\1n\1m\1" + "3"); // Magenta on brown
+		text = text.replace(/@66@/g, "\1n\1y\1" + "3"); // Yellow/brown on brown
+		text = text.replace(/@67@/g, "\1n\1w\1" + "3"); // White on brown
+		text = text.replace(/@68@/g, "\1h\1k\1" + "3"); // Bright black on brown
+		text = text.replace(/@69@/g, "\1h\1b\1" + "3"); // Bright blue on brown
+		text = text.replace(/@6[Aa]@/g, "\1h\1g\1" + "3"); // Bright breen on brown
+		text = text.replace(/@6[Bb]@/g, "\1h\1c\1" + "3"); // Bright cyan on brown
+		text = text.replace(/@6[Cc]@/g, "\1h\1r\1" + "3"); // Bright red on brown
+		text = text.replace(/@6[Dd]@/g, "\1h\1m\1" + "3"); // Bright magenta on brown
+		text = text.replace(/@6[Ee]@/g, "\1h\1y\1" + "3"); // Bright yellow on brown
+		text = text.replace(/@6[Ff]@/g, "\1h\1w\1" + "3"); // Bright white on brown
+
+		// White background
+		text = text.replace(/@70@/g, "\1n\1k\1" + "7"); // Black on white
+		text = text.replace(/@71@/g, "\1n\1b\1" + "7"); // Blue on white
+		text = text.replace(/@72@/g, "\1n\1g\1" + "7"); // Green on white
+		text = text.replace(/@73@/g, "\1n\1c\1" + "7"); // Cyan on white
+		text = text.replace(/@74@/g, "\1n\1r\1" + "7"); // Red on white
+		text = text.replace(/@75@/g, "\1n\1m\1" + "7"); // Magenta on white
+		text = text.replace(/@76@/g, "\1n\1y\1" + "7"); // Yellow/brown on white
+		text = text.replace(/@77@/g, "\1n\1w\1" + "7"); // White on white
+		text = text.replace(/@78@/g, "\1h\1k\1" + "7"); // Bright black on white
+		text = text.replace(/@79@/g, "\1h\1b\1" + "7"); // Bright blue on white
+		text = text.replace(/@7[Aa]@/g, "\1h\1g\1" + "7"); // Bright green on white
+		text = text.replace(/@7[Bb]@/g, "\1h\1c\1" + "7"); // Bright cyan on white
+		text = text.replace(/@7[Cc]@/g, "\1h\1r\1" + "7"); // Bright red on white
+		text = text.replace(/@7[Dd]@/g, "\1h\1m\1" + "7"); // Bright magenta on white
+		text = text.replace(/@7[Ee]@/g, "\1h\1y\1" + "7"); // Bright yellow on white
+		text = text.replace(/@7[Ff]@/g, "\1h\1w\1" + "7"); // Bright white on white
+
+		// Black background, blinking foreground
+		text = text.replace(/@80@/g, "\1n\1k\1" + "0\1i"); // Blinking black on black
+		text = text.replace(/@81@/g, "\1n\1b\1" + "0\1i"); // Blinking blue on black
+		text = text.replace(/@82@/g, "\1n\1g\1" + "0\1i"); // Blinking green on black
+		text = text.replace(/@83@/g, "\1n\1c\1" + "0\1i"); // Blinking cyan on black
+		text = text.replace(/@84@/g, "\1n\1r\1" + "0\1i"); // Blinking red on black
+		text = text.replace(/@85@/g, "\1n\1m\1" + "0\1i"); // Blinking magenta on black
+		text = text.replace(/@86@/g, "\1n\1y\1" + "0\1i"); // Blinking yellow/brown on black
+		text = text.replace(/@87@/g, "\1n\1w\1" + "0\1i"); // Blinking white on black
+		text = text.replace(/@88@/g, "\1h\1k\1" + "0\1i"); // Blinking bright black on black
+		text = text.replace(/@89@/g, "\1h\1b\1" + "0\1i"); // Blinking bright blue on black
+		text = text.replace(/@8[Aa]@/g, "\1h\1g\1" + "0\1i"); // Blinking bright green on black
+		text = text.replace(/@8[Bb]@/g, "\1h\1c\1" + "0\1i"); // Blinking bright cyan on black
+		text = text.replace(/@8[Cc]@/g, "\1h\1r\1" + "0\1i"); // Blinking bright red on black
+		text = text.replace(/@8[Dd]@/g, "\1h\1m\1" + "0\1i"); // Blinking bright magenta on black
+		text = text.replace(/@8[Ee]@/g, "\1h\1y\1" + "0\1i"); // Blinking bright yellow on black
+		text = text.replace(/@8[Ff]@/g, "\1h\1w\1" + "0\1i"); // Blinking bright white on black
+
+		// Blue background, blinking foreground
+		text = text.replace(/@90@/g, "\1n\1k\1" + "4\1i"); // Blinking black on blue
+		text = text.replace(/@91@/g, "\1n\1b\1" + "4\1i"); // Blinking blue on blue
+		text = text.replace(/@92@/g, "\1n\1g\1" + "4\1i"); // Blinking green on blue
+		text = text.replace(/@93@/g, "\1n\1c\1" + "4\1i"); // Blinking cyan on blue
+		text = text.replace(/@94@/g, "\1n\1r\1" + "4\1i"); // Blinking red on blue
+		text = text.replace(/@95@/g, "\1n\1m\1" + "4\1i"); // Blinking magenta on blue
+		text = text.replace(/@96@/g, "\1n\1y\1" + "4\1i"); // Blinking yellow/brown on blue
+		text = text.replace(/@97@/g, "\1n\1w\1" + "4\1i"); // Blinking white on blue
+		text = text.replace(/@98@/g, "\1h\1k\1" + "4\1i"); // Blinking bright black on blue
+		text = text.replace(/@99@/g, "\1h\1b\1" + "4\1i"); // Blinking bright blue on blue
+		text = text.replace(/@9[Aa]@/g, "\1h\1g\1" + "4\1i"); // Blinking bright green on blue
+		text = text.replace(/@9[Bb]@/g, "\1h\1c\1" + "4\1i"); // Blinking bright cyan on blue
+		text = text.replace(/@9[Cc]@/g, "\1h\1r\1" + "4\1i"); // Blinking bright red on blue
+		text = text.replace(/@9[Dd]@/g, "\1h\1m\1" + "4\1i"); // Blinking bright magenta on blue
+		text = text.replace(/@9[Ee]@/g, "\1h\1y\1" + "4\1i"); // Blinking bright yellow on blue
+		text = text.replace(/@9[Ff]@/g, "\1h\1w\1" + "4\1i"); // Blinking bright white on blue
+
+		// Green background, blinking foreground
+		text = text.replace(/@[Aa]0@/g, "\1n\1k\1" + "2\1i"); // Blinking black on green
+		text = text.replace(/@[Aa]1@/g, "\1n\1b\1" + "2\1i"); // Blinking blue on green
+		text = text.replace(/@[Aa]2@/g, "\1n\1g\1" + "2\1i"); // Blinking green on green
+		text = text.replace(/@[Aa]3@/g, "\1n\1c\1" + "2\1i"); // Blinking cyan on green
+		text = text.replace(/@[Aa]4@/g, "\1n\1r\1" + "2\1i"); // Blinking red on green
+		text = text.replace(/@[Aa]5@/g, "\1n\1m\1" + "2\1i"); // Blinking magenta on green
+		text = text.replace(/@[Aa]6@/g, "\1n\1y\1" + "2\1i"); // Blinking yellow/brown on green
+		text = text.replace(/@[Aa]7@/g, "\1n\1w\1" + "2\1i"); // Blinking white on green
+		text = text.replace(/@[Aa]8@/g, "\1h\1k\1" + "2\1i"); // Blinking bright black on green
+		text = text.replace(/@[Aa]9@/g, "\1h\1b\1" + "2\1i"); // Blinking bright blue on green
+		text = text.replace(/@[Aa][Aa]@/g, "\1h\1g\1" + "2\1i"); // Blinking bright green on green
+		text = text.replace(/@[Aa][Bb]@/g, "\1h\1c\1" + "2\1i"); // Blinking bright cyan on green
+		text = text.replace(/@[Aa][Cc]@/g, "\1h\1r\1" + "2\1i"); // Blinking bright red on green
+		text = text.replace(/@[Aa][Dd]@/g, "\1h\1m\1" + "2\1i"); // Blinking bright magenta on green
+		text = text.replace(/@[Aa][Ee]@/g, "\1h\1y\1" + "2\1i"); // Blinking bright yellow on green
+		text = text.replace(/@[Aa][Ff]@/g, "\1h\1w\1" + "2\1i"); // Blinking bright white on green
+
+		// Cyan background, blinking foreground
+		text = text.replace(/@[Bb]0@/g, "\1n\1k\1" + "6\1i"); // Blinking black on cyan
+		text = text.replace(/@[Bb]1@/g, "\1n\1b\1" + "6\1i"); // Blinking blue on cyan
+		text = text.replace(/@[Bb]2@/g, "\1n\1g\1" + "6\1i"); // Blinking green on cyan
+		text = text.replace(/@[Bb]3@/g, "\1n\1c\1" + "6\1i"); // Blinking cyan on cyan
+		text = text.replace(/@[Bb]4@/g, "\1n\1r\1" + "6\1i"); // Blinking red on cyan
+		text = text.replace(/@[Bb]5@/g, "\1n\1m\1" + "6\1i"); // Blinking magenta on cyan
+		text = text.replace(/@[Bb]6@/g, "\1n\1y\1" + "6\1i"); // Blinking yellow/brown on cyan
+		text = text.replace(/@[Bb]7@/g, "\1n\1w\1" + "6\1i"); // Blinking white on cyan
+		text = text.replace(/@[Bb]8@/g, "\1h\1k\1" + "6\1i"); // Blinking bright black on cyan
+		text = text.replace(/@[Bb]9@/g, "\1h\1b\1" + "6\1i"); // Blinking bright blue on cyan
+		text = text.replace(/@[Bb][Aa]@/g, "\1h\1g\1" + "6\1i"); // Blinking bright green on cyan
+		text = text.replace(/@[Bb][Bb]@/g, "\1h\1c\1" + "6\1i"); // Blinking bright cyan on cyan
+		text = text.replace(/@[Bb][Cc]@/g, "\1h\1r\1" + "6\1i"); // Blinking bright red on cyan
+		text = text.replace(/@[Bb][Dd]@/g, "\1h\1m\1" + "6\1i"); // Blinking bright magenta on cyan
+		text = text.replace(/@[Bb][Ee]@/g, "\1h\1y\1" + "6\1i"); // Blinking bright yellow on cyan
+		text = text.replace(/@[Bb][Ff]@/g, "\1h\1w\1" + "6\1i"); // Blinking bright white on cyan
+
+		// Red background, blinking foreground
+		text = text.replace(/@[Cc]0@/g, "\1n\1k\1" + "1\1i"); // Blinking black on red
+		text = text.replace(/@[Cc]1@/g, "\1n\1b\1" + "1\1i"); // Blinking blue on red
+		text = text.replace(/@[Cc]2@/g, "\1n\1g\1" + "1\1i"); // Blinking green on red
+		text = text.replace(/@[Cc]3@/g, "\1n\1c\1" + "1\1i"); // Blinking cyan on red
+		text = text.replace(/@[Cc]4@/g, "\1n\1r\1" + "1\1i"); // Blinking red on red
+		text = text.replace(/@[Cc]5@/g, "\1n\1m\1" + "1\1i"); // Blinking magenta on red
+		text = text.replace(/@[Cc]6@/g, "\1n\1y\1" + "1\1i"); // Blinking yellow/brown on red
+		text = text.replace(/@[Cc]7@/g, "\1n\1w\1" + "1\1i"); // Blinking white on red
+		text = text.replace(/@[Cc]8@/g, "\1h\1k\1" + "1\1i"); // Blinking bright black on red
+		text = text.replace(/@[Cc]9@/g, "\1h\1b\1" + "1\1i"); // Blinking bright blue on red
+		text = text.replace(/@[Cc][Aa]@/g, "\1h\1g\1" + "1\1i"); // Blinking bright green on red
+		text = text.replace(/@[Cc][Bb]@/g, "\1h\1c\1" + "1\1i"); // Blinking bright cyan on red
+		text = text.replace(/@[Cc][Cc]@/g, "\1h\1r\1" + "1\1i"); // Blinking bright red on red
+		text = text.replace(/@[Cc][Dd]@/g, "\1h\1m\1" + "1\1i"); // Blinking bright magenta on red
+		text = text.replace(/@[Cc][Ee]@/g, "\1h\1y\1" + "1\1i"); // Blinking bright yellow on red
+		text = text.replace(/@[Cc][Ff]@/g, "\1h\1w\1" + "1\1i"); // Blinking bright white on red
+
+		// Magenta background, blinking foreground
+		text = text.replace(/@[Dd]0@/g, "\1n\1k\1" + "5\1i"); // Blinking black on magenta
+		text = text.replace(/@[Dd]1@/g, "\1n\1b\1" + "5\1i"); // Blinking blue on magenta
+		text = text.replace(/@[Dd]2@/g, "\1n\1g\1" + "5\1i"); // Blinking green on magenta
+		text = text.replace(/@[Dd]3@/g, "\1n\1c\1" + "5\1i"); // Blinking cyan on magenta
+		text = text.replace(/@[Dd]4@/g, "\1n\1r\1" + "5\1i"); // Blinking red on magenta
+		text = text.replace(/@[Dd]5@/g, "\1n\1m\1" + "5\1i"); // Blinking magenta on magenta
+		text = text.replace(/@[Dd]6@/g, "\1n\1y\1" + "5\1i"); // Blinking yellow/brown on magenta
+		text = text.replace(/@[Dd]7@/g, "\1n\1w\1" + "5\1i"); // Blinking white on magenta
+		text = text.replace(/@[Dd]8@/g, "\1h\1k\1" + "5\1i"); // Blinking bright black on magenta
+		text = text.replace(/@[Dd]9@/g, "\1h\1b\1" + "5\1i"); // Blinking bright blue on magenta
+		text = text.replace(/@[Dd][Aa]@/g, "\1h\1g\1" + "5\1i"); // Blinking bright green on magenta
+		text = text.replace(/@[Dd][Bb]@/g, "\1h\1c\1" + "5\1i"); // Blinking bright cyan on magenta
+		text = text.replace(/@[Dd][Cc]@/g, "\1h\1r\1" + "5\1i"); // Blinking bright red on magenta
+		text = text.replace(/@[Dd][Dd]@/g, "\1h\1m\1" + "5\1i"); // Blinking bright magenta on magenta
+		text = text.replace(/@[Dd][Ee]@/g, "\1h\1y\1" + "5\1i"); // Blinking bright yellow on magenta
+		text = text.replace(/@[Dd][Ff]@/g, "\1h\1w\1" + "5\1i"); // Blinking bright white on magenta
+
+		// Brown background, blinking foreground
+		text = text.replace(/@[Ee]0@/g, "\1n\1k\1" + "3\1i"); // Blinking black on brown
+		text = text.replace(/@[Ee]1@/g, "\1n\1b\1" + "3\1i"); // Blinking blue on brown
+		text = text.replace(/@[Ee]2@/g, "\1n\1g\1" + "3\1i"); // Blinking green on brown
+		text = text.replace(/@[Ee]3@/g, "\1n\1c\1" + "3\1i"); // Blinking cyan on brown
+		text = text.replace(/@[Ee]4@/g, "\1n\1r\1" + "3\1i"); // Blinking red on brown
+		text = text.replace(/@[Ee]5@/g, "\1n\1m\1" + "3\1i"); // Blinking magenta on brown
+		text = text.replace(/@[Ee]6@/g, "\1n\1y\1" + "3\1i"); // Blinking yellow/brown on brown
+		text = text.replace(/@[Ee]7@/g, "\1n\1w\1" + "3\1i"); // Blinking white on brown
+		text = text.replace(/@[Ee]8@/g, "\1h\1k\1" + "3\1i"); // Blinking bright black on brown
+		text = text.replace(/@[Ee]9@/g, "\1h\1b\1" + "3\1i"); // Blinking bright blue on brown
+		text = text.replace(/@[Ee][Aa]@/g, "\1h\1g\1" + "3\1i"); // Blinking bright green on brown
+		text = text.replace(/@[Ee][Bb]@/g, "\1h\1c\1" + "3\1i"); // Blinking bright cyan on brown
+		text = text.replace(/@[Ee][Cc]@/g, "\1h\1r\1" + "3\1i"); // Blinking bright red on brown
+		text = text.replace(/@[Ee][Dd]@/g, "\1h\1m\1" + "3\1i"); // Blinking bright magenta on brown
+		text = text.replace(/@[Ee][Ee]@/g, "\1h\1y\1" + "3\1i"); // Blinking bright yellow on brown
+		text = text.replace(/@[Ee][Ff]@/g, "\1h\1w\1" + "3\1i"); // Blinking bright white on brown
+
+		// White background, blinking foreground
+		text = text.replace(/@[Ff]0@/g, "\1n\1k\1" + "7\1i"); // Blinking black on white
+		text = text.replace(/@[Ff]1@/g, "\1n\1b\1" + "7\1i"); // Blinking blue on white
+		text = text.replace(/@[Ff]2@/g, "\1n\1g\1" + "7\1i"); // Blinking green on white
+		text = text.replace(/@[Ff]3@/g, "\1n\1c\1" + "7\1i"); // Blinking cyan on white
+		text = text.replace(/@[Ff]4@/g, "\1n\1r\1" + "7\1i"); // Blinking red on white
+		text = text.replace(/@[Ff]5@/g, "\1n\1m\1" + "7\1i"); // Blinking magenta on white
+		text = text.replace(/@[Ff]6@/g, "\1n\1y\1" + "7\1i"); // Blinking yellow/brown on white
+		text = text.replace(/@[Ff]7@/g, "\1n\1w\1" + "7\1i"); // Blinking white on white
+		text = text.replace(/@[Ff]8@/g, "\1h\1k\1" + "7\1i"); // Blinking bright black on white
+		text = text.replace(/@[Ff]9@/g, "\1h\1b\1" + "7\1i"); // Blinking bright blue on white
+		text = text.replace(/@[Ff][Aa]@/g, "\1h\1g\1" + "7\1i"); // Blinking bright green on white
+		text = text.replace(/@[Ff][Bb]@/g, "\1h\1c\1" + "7\1i"); // Blinking bright cyan on white
+		text = text.replace(/@[Ff][Cc]@/g, "\1h\1r\1" + "7\1i"); // Blinking bright red on white
+		text = text.replace(/@[Ff][Dd]@/g, "\1h\1m\1" + "7\1i"); // Blinking bright magenta on white
+		text = text.replace(/@[Ff][Ee]@/g, "\1h\1y\1" + "7\1i"); // Blinking bright yellow on white
+		text = text.replace(/@[Ff][Ff]@/g, "\1h\1w\1" + "7\1i"); // Blinking bright white on white
+
+		return text;
+	}
+	else
+		return pText; // No Wildcat-style attribute codes found, so just return the text.
+}
+
+// Converts Celerity attribute codes to Synchronet attribute codes.
+//
+// Parameters:
+//  pText: A string containing the text to convert
+//
+// Return value: The text with the color codes converted
+function celerityAttrsToSyncAttrs(pText)
+{
+	// First, see if the text has any Celerity-style attribute codes at
+	// all.  We'll be performing a bunch of search & replace commands,
+	// so we don't want to do all that work for nothing.. :)
+	if (/\|[kbgcrmywdBGCRMYWS]/.test(pText))
+	{
+		// Using the \|S code (swap foreground & background)
+
+		// Blue background
+		var text = pText.replace(/\|b\|S\|k/g, "\1n\1k\1" + "4"); // Black on blue
+		text = text.replace(/\|b\|S\|b/g, "\1n\1b\1" + "4"); // Blue on blue
+		text = text.replace(/\|b\|S\|g/g, "\1n\1g\1" + "4"); // Green on blue
+		text = text.replace(/\|b\|S\|c/g, "\1n\1c\1" + "4"); // Cyan on blue
+		text = text.replace(/\|b\|S\|r/g, "\1n\1r\1" + "4"); // Red on blue
+		text = text.replace(/\|b\|S\|m/g, "\1n\1m\1" + "4"); // Magenta on blue
+		text = text.replace(/\|b\|S\|y/g, "\1n\1y\1" + "4"); // Yellow/brown on blue
+		text = text.replace(/\|b\|S\|w/g, "\1n\1w\1" + "4"); // White on blue
+		text = text.replace(/\|b\|S\|d/g, "\1h\1k\1" + "4"); // Bright black on blue
+		text = text.replace(/\|b\|S\|B/g, "\1h\1b\1" + "4"); // Bright blue on blue
+		text = text.replace(/\|b\|S\|G/g, "\1h\1g\1" + "4"); // Bright green on blue
+		text = text.replace(/\|b\|S\|C/g, "\1h\1c\1" + "4"); // Bright cyan on blue
+		text = text.replace(/\|b\|S\|R/g, "\1h\1r\1" + "4"); // Bright red on blue
+		text = text.replace(/\|b\|S\|M/g, "\1h\1m\1" + "4"); // Bright magenta on blue
+		text = text.replace(/\|b\|S\|Y/g, "\1h\1y\1" + "4"); // Yellow on blue
+		text = text.replace(/\|b\|S\|W/g, "\1h\1w\1" + "4"); // Bright white on blue
+
+		// Green background
+		text = text.replace(/\|g\|S\|k/g, "\1n\1k\1" + "2"); // Black on green
+		text = text.replace(/\|g\|S\|b/g, "\1n\1b\1" + "2"); // Blue on green
+		text = text.replace(/\|g\|S\|g/g, "\1n\1g\1" + "2"); // Green on green
+		text = text.replace(/\|g\|S\|c/g, "\1n\1c\1" + "2"); // Cyan on green
+		text = text.replace(/\|g\|S\|r/g, "\1n\1r\1" + "2"); // Red on green
+		text = text.replace(/\|g\|S\|m/g, "\1n\1m\1" + "2"); // Magenta on green
+		text = text.replace(/\|g\|S\|y/g, "\1n\1y\1" + "2"); // Yellow/brown on green
+		text = text.replace(/\|g\|S\|w/g, "\1n\1w\1" + "2"); // White on green
+		text = text.replace(/\|g\|S\|d/g, "\1h\1k\1" + "2"); // Bright black on green
+		text = text.replace(/\|g\|S\|B/g, "\1h\1b\1" + "2"); // Bright blue on green
+		text = text.replace(/\|g\|S\|G/g, "\1h\1g\1" + "2"); // Bright green on green
+		text = text.replace(/\|g\|S\|C/g, "\1h\1c\1" + "2"); // Bright cyan on green
+		text = text.replace(/\|g\|S\|R/g, "\1h\1r\1" + "2"); // Bright red on green
+		text = text.replace(/\|g\|S\|M/g, "\1h\1m\1" + "2"); // Bright magenta on green
+		text = text.replace(/\|g\|S\|Y/g, "\1h\1y\1" + "2"); // Yellow on green
+		text = text.replace(/\|g\|S\|W/g, "\1h\1w\1" + "2"); // Bright white on green
+
+		// Cyan background
+		text = text.replace(/\|c\|S\|k/g, "\1n\1k\1" + "6"); // Black on cyan
+		text = text.replace(/\|c\|S\|b/g, "\1n\1b\1" + "6"); // Blue on cyan
+		text = text.replace(/\|c\|S\|g/g, "\1n\1g\1" + "6"); // Green on cyan
+		text = text.replace(/\|c\|S\|c/g, "\1n\1c\1" + "6"); // Cyan on cyan
+		text = text.replace(/\|c\|S\|r/g, "\1n\1r\1" + "6"); // Red on cyan
+		text = text.replace(/\|c\|S\|m/g, "\1n\1m\1" + "6"); // Magenta on cyan
+		text = text.replace(/\|c\|S\|y/g, "\1n\1y\1" + "6"); // Yellow/brown on cyan
+		text = text.replace(/\|c\|S\|w/g, "\1n\1w\1" + "6"); // White on cyan
+		text = text.replace(/\|c\|S\|d/g, "\1h\1k\1" + "6"); // Bright black on cyan
+		text = text.replace(/\|c\|S\|B/g, "\1h\1b\1" + "6"); // Bright blue on cyan
+		text = text.replace(/\|c\|S\|G/g, "\1h\1g\1" + "6"); // Bright green on cyan
+		text = text.replace(/\|c\|S\|C/g, "\1h\1c\1" + "6"); // Bright cyan on cyan
+		text = text.replace(/\|c\|S\|R/g, "\1h\1r\1" + "6"); // Bright red on cyan
+		text = text.replace(/\|c\|S\|M/g, "\1h\1m\1" + "6"); // Bright magenta on cyan
+		text = text.replace(/\|c\|S\|Y/g, "\1h\1y\1" + "6"); // Yellow on cyan
+		text = text.replace(/\|c\|S\|W/g, "\1h\1w\1" + "6"); // Bright white on cyan
+
+		// Red background
+		text = text.replace(/\|r\|S\|k/g, "\1n\1k\1" + "1"); // Black on red
+		text = text.replace(/\|r\|S\|b/g, "\1n\1b\1" + "1"); // Blue on red
+		text = text.replace(/\|r\|S\|g/g, "\1n\1g\1" + "1"); // Green on red
+		text = text.replace(/\|r\|S\|c/g, "\1n\1c\1" + "1"); // Cyan on red
+		text = text.replace(/\|r\|S\|r/g, "\1n\1r\1" + "1"); // Red on red
+		text = text.replace(/\|r\|S\|m/g, "\1n\1m\1" + "1"); // Magenta on red
+		text = text.replace(/\|r\|S\|y/g, "\1n\1y\1" + "1"); // Yellow/brown on red
+		text = text.replace(/\|r\|S\|w/g, "\1n\1w\1" + "1"); // White on red
+		text = text.replace(/\|r\|S\|d/g, "\1h\1k\1" + "1"); // Bright black on red
+		text = text.replace(/\|r\|S\|B/g, "\1h\1b\1" + "1"); // Bright blue on red
+		text = text.replace(/\|r\|S\|G/g, "\1h\1g\1" + "1"); // Bright green on red
+		text = text.replace(/\|r\|S\|C/g, "\1h\1c\1" + "1"); // Bright cyan on red
+		text = text.replace(/\|r\|S\|R/g, "\1h\1r\1" + "1"); // Bright red on red
+		text = text.replace(/\|r\|S\|M/g, "\1h\1m\1" + "1"); // Bright magenta on red
+		text = text.replace(/\|r\|S\|Y/g, "\1h\1y\1" + "1"); // Yellow on red
+		text = text.replace(/\|r\|S\|W/g, "\1h\1w\1" + "1"); // Bright white on red
+
+		// Magenta background
+		text = text.replace(/\|m\|S\|k/g, "\1n\1k\1" + "5"); // Black on magenta
+		text = text.replace(/\|m\|S\|b/g, "\1n\1b\1" + "5"); // Blue on magenta
+		text = text.replace(/\|m\|S\|g/g, "\1n\1g\1" + "5"); // Green on magenta
+		text = text.replace(/\|m\|S\|c/g, "\1n\1c\1" + "5"); // Cyan on magenta
+		text = text.replace(/\|m\|S\|r/g, "\1n\1r\1" + "5"); // Red on magenta
+		text = text.replace(/\|m\|S\|m/g, "\1n\1m\1" + "5"); // Magenta on magenta
+		text = text.replace(/\|m\|S\|y/g, "\1n\1y\1" + "5"); // Yellow/brown on magenta
+		text = text.replace(/\|m\|S\|w/g, "\1n\1w\1" + "5"); // White on magenta
+		text = text.replace(/\|m\|S\|d/g, "\1h\1k\1" + "5"); // Bright black on magenta
+		text = text.replace(/\|m\|S\|B/g, "\1h\1b\1" + "5"); // Bright blue on magenta
+		text = text.replace(/\|m\|S\|G/g, "\1h\1g\1" + "5"); // Bright green on magenta
+		text = text.replace(/\|m\|S\|C/g, "\1h\1c\1" + "5"); // Bright cyan on magenta
+		text = text.replace(/\|m\|S\|R/g, "\1h\1r\1" + "5"); // Bright red on magenta
+		text = text.replace(/\|m\|S\|M/g, "\1h\1m\1" + "5"); // Bright magenta on magenta
+		text = text.replace(/\|m\|S\|Y/g, "\1h\1y\1" + "5"); // Yellow on magenta
+		text = text.replace(/\|m\|S\|W/g, "\1h\1w\1" + "5"); // Bright white on magenta
+
+		// Brown background
+		text = text.replace(/\|y\|S\|k/g, "\1n\1k\1" + "3"); // Black on brown
+		text = text.replace(/\|y\|S\|b/g, "\1n\1b\1" + "3"); // Blue on brown
+		text = text.replace(/\|y\|S\|g/g, "\1n\1g\1" + "3"); // Green on brown
+		text = text.replace(/\|y\|S\|c/g, "\1n\1c\1" + "3"); // Cyan on brown
+		text = text.replace(/\|y\|S\|r/g, "\1n\1r\1" + "3"); // Red on brown
+		text = text.replace(/\|y\|S\|m/g, "\1n\1m\1" + "3"); // Magenta on brown
+		text = text.replace(/\|y\|S\|y/g, "\1n\1y\1" + "3"); // Yellow/brown on brown
+		text = text.replace(/\|y\|S\|w/g, "\1n\1w\1" + "3"); // White on brown
+		text = text.replace(/\|y\|S\|d/g, "\1h\1k\1" + "3"); // Bright black on brown
+		text = text.replace(/\|y\|S\|B/g, "\1h\1b\1" + "3"); // Bright blue on brown
+		text = text.replace(/\|y\|S\|G/g, "\1h\1g\1" + "3"); // Bright green on brown
+		text = text.replace(/\|y\|S\|C/g, "\1h\1c\1" + "3"); // Bright cyan on brown
+		text = text.replace(/\|y\|S\|R/g, "\1h\1r\1" + "3"); // Bright red on brown
+		text = text.replace(/\|y\|S\|M/g, "\1h\1m\1" + "3"); // Bright magenta on brown
+		text = text.replace(/\|y\|S\|Y/g, "\1h\1y\1" + "3"); // Yellow on brown
+		text = text.replace(/\|y\|S\|W/g, "\1h\1w\1" + "3"); // Bright white on brown
+
+		// White background
+		text = text.replace(/\|w\|S\|k/g, "\1n\1k\1" + "7"); // Black on white
+		text = text.replace(/\|w\|S\|b/g, "\1n\1b\1" + "7"); // Blue on white
+		text = text.replace(/\|w\|S\|g/g, "\1n\1g\1" + "7"); // Green on white
+		text = text.replace(/\|w\|S\|c/g, "\1n\1c\1" + "7"); // Cyan on white
+		text = text.replace(/\|w\|S\|r/g, "\1n\1r\1" + "7"); // Red on white
+		text = text.replace(/\|w\|S\|m/g, "\1n\1m\1" + "7"); // Magenta on white
+		text = text.replace(/\|w\|S\|y/g, "\1n\1y\1" + "7"); // Yellow/brown on white
+		text = text.replace(/\|w\|S\|w/g, "\1n\1w\1" + "7"); // White on white
+		text = text.replace(/\|w\|S\|d/g, "\1h\1k\1" + "7"); // Bright black on white
+		text = text.replace(/\|w\|S\|B/g, "\1h\1b\1" + "7"); // Bright blue on white
+		text = text.replace(/\|w\|S\|G/g, "\1h\1g\1" + "7"); // Bright green on white
+		text = text.replace(/\|w\|S\|C/g, "\1h\1c\1" + "7"); // Bright cyan on white
+		text = text.replace(/\|w\|S\|R/g, "\1h\1r\1" + "7"); // Bright red on white
+		text = text.replace(/\|w\|S\|M/g, "\1h\1m\1" + "7"); // Bright magenta on white
+		text = text.replace(/\|w\|S\|Y/g, "\1h\1y\1" + "7"); // Yellow on white
+		text = text.replace(/\|w\|S\|W/g, "\1h\1w\1" + "7"); // Bright white on white
+
+		// Colors on black background
+		text = text.replace(/\|k/g, "\1n\1k\1" + "0");  // Black on black
+		text = text.replace(/\|k\|S\|k/g, "\1n\1k\1" + "0"); // Black on black
+		text = text.replace(/\|b/g, "\1n\1b\1" + "0");       // Blue on black
+		text = text.replace(/\|k\|S\|b/g, "\1n\1b\1" + "0"); // Blue on black
+		text = text.replace(/\|g/g, "\1n\1g\1" + "0");       // Green on black
+		text = text.replace(/\|k\|S\|g/g, "\1n\1g\1" + "0"); // Green on black
+		text = text.replace(/\|c/g, "\1n\1c\1" + "0");       // Cyan on black
+		text = text.replace(/\|k\|S\|c/g, "\1n\1c\1" + "0"); // Cyan on black
+		text = text.replace(/\|r/g, "\1n\1r\1" + "0");       // Red on black
+		text = text.replace(/\|k\|S\|r/g, "\1n\1r\1" + "0"); // Red on black
+		text = text.replace(/\|m/g, "\1n\1m\1" + "0");       // Magenta on black
+		text = text.replace(/\|k\|S\|m/g, "\1n\1m\1" + "0"); // Magenta on black
+		text = text.replace(/\|y/g, "\1n\1y\1" + "0");       // Yellow/brown on black
+		text = text.replace(/\|k\|S\|y/g, "\1n\1y\1" + "0"); // Yellow/brown on black
+		text = text.replace(/\|w/g, "\1n\1w\1" + "0");       // White on black
+		text = text.replace(/\|k\|S\|w/g, "\1n\1w\1" + "0"); // White on black
+		text = text.replace(/\|d/g, "\1h\1k\1" + "0");       // Bright black on black
+		text = text.replace(/\|k\|S\|d/g, "\1h\1k\1" + "0"); // Bright black on black
+		text = text.replace(/\|B/g, "\1h\1b\1" + "0");       // Bright blue on black
+		text = text.replace(/\|k\|S\|B/g, "\1h\1b\1" + "0"); // Bright blue on black
+		text = text.replace(/\|G/g, "\1h\1g\1" + "0");       // Bright green on black
+		text = text.replace(/\|k\|S\|G/g, "\1h\1g\1" + "0"); // Bright green on black
+		text = text.replace(/\|C/g, "\1h\1c\1" + "0");       // Bright cyan on black
+		text = text.replace(/\|k\|S\|C/g, "\1h\1c\1" + "0"); // Bright cyan on black
+		text = text.replace(/\|R/g, "\1h\1r\1" + "0");       // Bright red on black
+		text = text.replace(/\|k\|S\|R/g, "\1h\1r\1" + "0"); // Bright red on black
+		text = text.replace(/\|M/g, "\1h\1m\1" + "0");       // Bright magenta on black
+		text = text.replace(/\|k\|S\|M/g, "\1h\1m\1" + "0"); // Bright magenta on black
+		text = text.replace(/\|Y/g, "\1h\1y\1" + "0");       // Yellow on black
+		text = text.replace(/\|k\|S\|Y/g, "\1h\1y\1" + "0"); // Yellow on black
+		text = text.replace(/\|W/g, "\1h\1w\1" + "0");       // Bright white on black
+		text = text.replace(/\|k\|S\|W/g, "\1h\1w\1" + "0"); // Bright white on black
+
+		return text;
+	}
+	else
+		return pText; // No Celerity-style attribute codes found, so just return the text.
+}
+
+// Converts Renegade attribute (color) codes to Synchronet attribute codes.
+//
+// Parameters:
+//  pText: A string containing the text to convert
+//
+// Return value: The text with the color codes converted
+function renegadeAttrsToSyncAttrs(pText)
+{
+	// First, see if the text has any Renegade-style attribute codes at
+	// all.  We'll be performing a bunch of search & replace commands,
+	// so we don't want to do all that work for nothing.. :)
+	if (/\|[0-3][0-9]/.test(pText))
+	{
+		var text = pText.replace(/\|00/g, "\1n\1k"); // Normal black
+		text = text.replace(/\|01/g, "\1n\1b"); // Normal blue
+		text = text.replace(/\|02/g, "\1n\1g"); // Normal green
+		text = text.replace(/\|03/g, "\1n\1c"); // Normal cyan
+		text = text.replace(/\|04/g, "\1n\1r"); // Normal red
+		text = text.replace(/\|05/g, "\1n\1m"); // Normal magenta
+		text = text.replace(/\|06/g, "\1n\1y"); // Normal brown
+		text = text.replace(/\|07/g, "\1n\1w"); // Normal white
+		text = text.replace(/\|08/g, "\1n\1k\1h"); // High intensity black
+		text = text.replace(/\|09/g, "\1n\1b\1h"); // High intensity blue
+		text = text.replace(/\|10/g, "\1n\1g\1h"); // High intensity green
+		text = text.replace(/\|11/g, "\1n\1c\1h"); // High intensity cyan
+		text = text.replace(/\|12/g, "\1n\1r\1h"); // High intensity red
+		text = text.replace(/\|13/g, "\1n\1m\1h"); // High intensity magenta
+		text = text.replace(/\|14/g, "\1n\1y\1h"); // Yellow (high intensity brown)
+		text = text.replace(/\|15/g, "\1n\1w\1h"); // High intensity white
+		text = text.replace(/\|16/g, "\1" + "0"); // Background black
+		text = text.replace(/\|17/g, "\1" + "4"); // Background blue
+		text = text.replace(/\|18/g, "\1" + "2"); // Background green
+		text = text.replace(/\|19/g, "\1" + "6"); // Background cyan
+		text = text.replace(/\|20/g, "\1" + "1"); // Background red
+		text = text.replace(/\|21/g, "\1" + "5"); // Background magenta
+		text = text.replace(/\|22/g, "\1" + "3"); // Background brown
+		text = text.replace(/\|23/g, "\1" + "7"); // Background white
+		text = text.replace(/\|24/g, "\1i\1w\1" + "0"); // Blinking white on black
+		text = text.replace(/\|25/g, "\1i\1w\1" + "4"); // Blinking white on blue
+		text = text.replace(/\|26/g, "\1i\1w\1" + "2"); // Blinking white on green
+		text = text.replace(/\|27/g, "\1i\1w\1" + "6"); // Blinking white on cyan
+		text = text.replace(/\|28/g, "\1i\1w\1" + "1"); // Blinking white on red
+		text = text.replace(/\|29/g, "\1i\1w\1" + "5"); // Blinking white on magenta
+		text = text.replace(/\|30/g, "\1i\1w\1" + "3"); // Blinking white on yellow/brown
+		text = text.replace(/\|31/g, "\1i\1w\1" + "7"); // Blinking white on white
+		return text;
+	}
+	else
+		return pText; // No Renegade-style attribute codes found, so just return the text.
+}
+
+// Converts ANSI attribute codes to Synchronet attribute codes.  This
+// is incomplete and doesn't convert all ANSI codes perfectly to Synchronet
+// attribute codes.
+//
+// Parameters:
+//  pText: A string containing the text to convert
+//
+// Return value: The text with ANSI codes converted to Synchronet attribute codes
+function ANSIAttrsToSyncAttrs(pText)
+{
+	// TODO: Test & update this some more..  Not sure if this is working 100% right.
+
+	// Web pages with ANSI code information:
+	// http://pueblo.sourceforge.net/doc/manual/ansi_color_codes.html
+	// http://ascii-table.com/ansi-escape-sequences.php
+	// http://stackoverflow.com/questions/4842424/list-of-ansi-color-escape-sequences
+
+	// If the string has ANSI codes in it, then go ahead and replace ANSI
+	// codes with Synchronet attribute codes.
+	if (textHasANSICodes(pText))
+	{
+		// Attributes
+		var txt = pText.replace(/\033\[0[mM]/g, "\1n"); // All attributes off
+		txt = txt.replace(/\033\[1[mM]/g, "\1h"); // Bold on (use high intensity)
+		txt = txt.replace(/\033\[5[mM]/g, "\1i"); // Blink on
+		// Foreground colors
+		txt = txt.replace(/\033\[30[mM]/g, "\1k"); // Black foreground
+		txt = txt.replace(/\033\[31[mM]/g, "\1r"); // Red foreground
+		txt = txt.replace(/\033\[32[mM]/g, "\1g"); // Green foreground
+		txt = txt.replace(/\033\[33[mM]/g, "\1y"); // Yellow foreground
+		txt = txt.replace(/\033\[34[mM]/g, "\1b"); // Blue foreground
+		txt = txt.replace(/\033\[35[mM]/g, "\1m"); // Magenta foreground
+		txt = txt.replace(/\033\[36[mM]/g, "\1c"); // Cyan foreground
+		txt = txt.replace(/\033\[37[mM]/g, "\1w"); // White foreground
+		// Background colors
+		txt = txt.replace(/\033\[40[mM]/g, "\1" + "0"); // Black background
+		txt = txt.replace(/\033\[41[mM]/g, "\1" + "1"); // Red background
+		txt = txt.replace(/\033\[42[mM]/g, "\1" + "2"); // Green background
+		txt = txt.replace(/\033\[43[mM]/g, "\1" + "3"); // Yellow background
+		txt = txt.replace(/\033\[44[mM]/g, "\1" + "4"); // Blue background
+		txt = txt.replace(/\033\[45[mM]/g, "\1" + "5"); // Magenta background
+		txt = txt.replace(/\033\[46[mM]/g, "\1" + "6"); // Cyan background
+		txt = txt.replace(/\033\[47[mM]/g, "\1" + "7"); // White background
+		// Convert ;-delimited modes (such as \033[Value;...;Valuem)
+		txt = ANSIMultiConvertToSyncCodes(txt);
+		// Remove ANSI codes that are not wanted (such as moving the cursor, etc.)
+		txt = txt.replace(/\033\[[0-9]+[aA]/g, ""); // Cursor up
+		txt = txt.replace(/\033\[[0-9]+[bB]/g, ""); // Cursor down
+		txt = txt.replace(/\033\[[0-9]+[cC]/g, ""); // Cursor forward
+		txt = txt.replace(/\033\[[0-9]+[dD]/g, ""); // Cursor backward
+		txt = txt.replace(/\033\[[0-9]+;[0-9]+[hH]/g, ""); // Cursor position
+		txt = txt.replace(/\033\[[0-9]+;[0-9]+[fF]/g, ""); // Cursor position
+		txt = txt.replace(/\033\[[sS]/g, ""); // Restore cursor position
+		txt = txt.replace(/\033\[2[jJ]/g, ""); // Erase display
+		txt = txt.replace(/\033\[[kK]/g, ""); // Erase line
+		txt = txt.replace(/\033\[=[0-9]+[hH]/g, ""); // Set various screen modes
+		txt = txt.replace(/\033\[=[0-9]+[lL]/g, ""); // Reset various screen modes
+		return txt;
+	}
+	else
+		return pText; // No ANSI codes found, so just return the text.
+}
+
+// Returns whether or not some text has any ANSI codes in it.
+//
+// Parameters:
+//  pText: The text to test
+//
+// Return value: Boolean - Whether or not the text has ANSI codes in it
+function textHasANSICodes(pText)
+{
+	
+	return(/\033\[[0-9]+[mM]/.test(pText) || /\033\[[0-9]+(;[0-9]+)+[mM]/.test(pText) ||
+	       /\033\[[0-9]+[aAbBcCdD]/.test(pText) || /\033\[[0-9]+;[0-9]+[hHfF]/.test(pText) ||
+	       /\033\[[sSuUkK]/.test(pText) || /\033\[2[jJ]/.test(pText));
+	/*
+	var regex1 = new RegExp(ascii(27) + "\[[0-9]+[mM]");
+	var regex2 = new RegExp(ascii(27) + "\[[0-9]+(;[0-9]+)+[mM]");
+	var regex3 = new RegExp(ascii(27) + "\[[0-9]+[aAbBcCdD]");
+	var regex4 = new RegExp(ascii(27) + "\[[0-9]+;[0-9]+[hHfF]");
+	var regex5 = new RegExp(ascii(27) + "\[[sSuUkK]");
+	var regex6 = new RegExp(ascii(27) + "\[2[jJ]");
+	return(regex1.test(pText) || regex2.test(pText) || regex3.test(pText) ||
+	       regex4.test(pText) || regex5.test(pText) || regex6.test(pText));
+	*/
+}
+
+// Converts ANSI ;-delimited modes (such as alue;...;Valuem) to Synchronet
+// attribute codes
+//
+// Parameters:
+//  pText: The text with ANSI ;-delimited modes to convert
+//
+// Return value: The text with ANSI ;-delimited codes converted to Synchronet attributes
+function ANSIMultiConvertToSyncCodes(pText)
+{
+	var multiMatches = pText.match(/\033\[[0-9]+(;[0-9]+)+m/g);
+	if (multiMatches == null)
+		return pText;
+	var updatedText = pText;
+	for (var i = 0; i < multiMatches.length; ++i)
+	{
+		// Copy the string, with the \033[ removed from the beginning and the
+		// trailing 'm' removed
+		var text = multiMatches[i].substr(2);
+		text = text.substr(0, text.length-1);
+		var codes = text.split(";");
+		var syncCodes = "";
+		for (var idx = 0; idx < codes.length; ++idx)
+		{
+			if (codes[idx] == "0") // All attributes off
+				syncCodes += "\1n";
+			else if (codes[idx] == "1") // Bold on (high intensity)
+				syncCodes += "\1h";
+			else if (codes[idx] == "5") // Blink on
+				syncCodes += "\1i";
+			else if (codes[idx] == "30") // Black foreground
+				syncCodes += "\1k";
+			else if (codes[idx] == "31") // Red foreground
+				syncCodes += "\1r";
+			else if (codes[idx] == "32") // Green foreground
+				syncCodes += "\1g";
+			else if (codes[idx] == "33") // Yellow foreground
+				syncCodes += "\1y";
+			else if (codes[idx] == "34") // Blue foreground
+				syncCodes += "\1b";
+			else if (codes[idx] == "35") // Magenta foreground
+				syncCodes += "\1m";
+			else if (codes[idx] == "36") // Cyan foreground
+				syncCodes += "\1c";
+			else if (codes[idx] == "37") // White foreground
+				syncCodes += "\1w";
+			else if (codes[idx] == "40") // Black background
+				syncCodes += "\1" + "0";
+			else if (codes[idx] == "41") // Red background
+				syncCodes += "\1" + "1";
+			else if (codes[idx] == "42") // Green background
+				syncCodes += "\1" + "2";
+			else if (codes[idx] == "43") // Yellow background
+				syncCodes += "\1" + "3";
+			else if (codes[idx] == "44") // Blue background
+				syncCodes += "\1" + "4";
+			else if (codes[idx] == "45") // Magenta background
+				syncCodes += "\1" + "5";
+			else if (codes[idx] == "46") // Cyan background
+				syncCodes += "\1" + "6";
+			else if (codes[idx] == "47") // White background
+				syncCodes += "\1" + "7";
+		}
+		updatedText = updatedText.replace(multiMatches[i], syncCodes);
+	}
+	return updatedText;
+}
+
+// Converts non-Synchronet attribute codes in text to Synchronet attribute
+// codes according to the toggle options in SCFG > Message Options > Extra
+// Attribute Codes
+//
+// Parameters:
+//  pText: The text to be converted
+//
+// Return value: The text with various other system attribute codes converted
+//               to Synchronet attribute codes, or not, depending on the toggle
+//               options in Extra Attribute Codes in SCFG
+function convertAttrsToSyncPerSysCfg(pText)
+{
+	// Convert any ANSI codes to Synchronet attribute codes.
+	// Then convert other BBS attribute codes to Synchronet attribute
+	// codes according to the current system configuration.
+	var convertedText = ANSIAttrsToSyncAttrs(pText);
+	if ((system.settings & SYS_RENEGADE) == SYS_RENEGADE)
+		convertedText = renegadeAttrsToSyncAttrs(convertedText);
+	if ((system.settings & SYS_WWIV) == SYS_WWIV)
+		convertedText = WWIVAttrsToSyncAttrs(convertedText);
+	if ((system.settings & SYS_CELERITY) == SYS_CELERITY)
+		convertedText = celerityAttrsToSyncAttrs(convertedText);
+	if ((system.settings & SYS_PCBOARD) == SYS_PCBOARD)
+		convertedText = PCBoardAttrsToSyncAttrs(convertedText);
+	if ((system.settings & SYS_WILDCAT) == SYS_WILDCAT)
+		convertedText = wildcatAttrsToSyncAttrs(convertedText);
+	return convertedText;
+}
+
+// Converts Synchronet attribute codes to ANSI ;-delimited modes (such as \033[Value;...;Valuem)
+//
+// Parameters:
+//  pText: The text with Synchronet codes to convert
+//
+// Return value: The text with Synchronet attributes converted to ANSI ;-delimited codes
+function syncAttrCodesToANSI(pText)
+{
+	// First, see if the text has any Synchronet attribute codes at
+	// all.  We'll be performing a bunch of search & replace commands,
+	// so we don't want to do all that work for nothing.. :)
+	if (hasSyncAttrCodes(pText))
+	{
+		var ANSIESCCodeStart = "\033[";
+		var newText = pText.replace(/\1n/gi, ANSIESCCodeStart + "0m"); // Normal
+		newText = newText.replace(/\1-/gi, ANSIESCCodeStart + "0m"); // Normal
+		newText = newText.replace(/\1_/gi, ANSIESCCodeStart + "0m"); // Normal
+		newText = newText.replace(/\1h/gi, ANSIESCCodeStart + "1m"); // High intensity/bold
+		newText = newText.replace(/\1i/gi, ANSIESCCodeStart + "5m"); // Blinking on
+		newText = newText.replace(/\1f/gi, ANSIESCCodeStart + "5m"); // Blinking on
+		newText = newText.replace(/\1k/gi, ANSIESCCodeStart + "30m"); // Black foreground
+		newText = newText.replace(/\1r/gi, ANSIESCCodeStart + "31m"); // Red foreground
+		newText = newText.replace(/\1g/gi, ANSIESCCodeStart + "32m"); // Green foreground
+		newText = newText.replace(/\1y/gi, ANSIESCCodeStart + "33m"); // Yellow/brown foreground
+		newText = newText.replace(/\1b/gi, ANSIESCCodeStart + "34m"); // Blue foreground
+		newText = newText.replace(/\1m/gi, ANSIESCCodeStart + "35m"); // Magenta foreground
+		newText = newText.replace(/\1c/gi, ANSIESCCodeStart + "36m"); // Cyan foreground
+		newText = newText.replace(/\1w/gi, ANSIESCCodeStart + "37m"); // White foreground
+		newText = newText.replace(/\1[0]/gi, ANSIESCCodeStart + "40m"); // Black background
+		newText = newText.replace(/\1[1]/gi, ANSIESCCodeStart + "41m"); // Red background
+		newText = newText.replace(/\1[2]/gi, ANSIESCCodeStart + "42m"); // Green background
+		newText = newText.replace(/\1[3]/gi, ANSIESCCodeStart + "43m"); // Yellow/brown background
+		newText = newText.replace(/\1[4]/gi, ANSIESCCodeStart + "44m"); // Blue background
+		newText = newText.replace(/\1[5]/gi, ANSIESCCodeStart + "45m"); // Magenta background
+		newText = newText.replace(/\1[6]/gi, ANSIESCCodeStart + "46m"); // Cyan background
+		newText = newText.replace(/\1[7]/gi, ANSIESCCodeStart + "47m"); // White background
+		return newText;
+	}
+	else
+		return pText; // No Synchronet-style attribute codes found, so just return the text.
+}
\ No newline at end of file
diff --git a/exec/load/dd_lightbar_menu.js b/exec/load/dd_lightbar_menu.js
index 1c0e7e4d8d69115a2d1f9b8b7d938ca344ec2cd3..2046978fce9e291c7ce5bfffa099fd2c45cdf4f7 100644
--- a/exec/load/dd_lightbar_menu.js
+++ b/exec/load/dd_lightbar_menu.js
@@ -281,6 +281,11 @@ menu with a custom OnItemSelect() function specified and you want the menu to co
 be displayed allowing the user to select an item.
 lbMenu.exitOnItemSelect = false;
 
+OnItemNav is a function that is called when the user navigates to a new item (i.e., via
+the up or down arrow, PageUp, PageDown, Home, End, etc.).  Its parameters are the old
+item index and the new item index.
+this.OnItemNav = function(pOldItemIdx, pNewItemIdx) { }
+
 The 'key down' behavior can be called explicitly, if needed, by calling the DoKeyDown() function.
 It takes 2 parameters: An object of selected item indexes (as passed to GetVal()) and, optionally,
 the pre-calculated number of items.
@@ -509,6 +514,10 @@ function DDLightbarMenu(pX, pY, pWidth, pHeight)
 	this.GetColorForItem = DDLightbarMenu_GetColorForItem;
 	this.GetSelectedColorForItem = DDLightbarMenu_GetSelectedColorForItem;
 	this.SetSelectedItemIdx = DDLightbarMenu_SetSelectedItemIdx;
+	this.GetBottomItemIdx = DDLightbarMenu_GetBottomItemIdx;
+	this.GetTopDisplayedItemPos = DDLightbarMenu_GetTopDisplayedItemPos;
+	this.GetBottomDisplayedItemPos = DDLightbarMenu_GetBottomDisplayedItemPos;
+	this.ScreenRowForItem = DDLightbarMenu_ScreenRowForItem;
 
 	// ValidateSelectItem is a function for validating that the user can select an item.
 	// It takes the selected item's return value and returns a boolean to signify whether
@@ -524,6 +533,10 @@ function DDLightbarMenu(pX, pY, pWidth, pHeight)
 	//             is possible when multi-select is enabled.
 	this.OnItemSelect = function(pItemRetval, pSelected) { }
 
+	// OnItemNav is a function that is called when the user navigates to
+	// new item (i.e., up/down arrow, pageUp, pageDown, home, end)
+	this.OnItemNav = function(pOldItemIdx, pNewItemIdx) { }
+
 	// Set some things based on the parameters passed in
 	if ((typeof(pX) == "number") && (typeof(pY) == "number"))
 		this.SetPos(pX, pY);
@@ -1027,8 +1040,15 @@ function DDLightbarMenu_DrawPartial(pStartX, pStartY, pWidth, pHeight, pSelected
 	if (this.scrollbarEnabled && !this.CanShowAllItemsInWindow())
 	{
 		var scrollbarCol = this.borderEnabled ? this.pos.x + this.size.width - 2 : this.pos.x + this.size.width - 1;
-		if (this.pos.x + pStartX + width - 1 >= scrollbarCol) // The last column drawn includes the scrollbar
-			--itemLen;
+		// If the rightmost column is at or past the scrollbar column,
+		// then subtract from the item length so that we don't overwrite
+		// the scrollbar.
+		var rightmostCol = this.pos.x + pStartX + width - 2;
+		if (rightmostCol >= scrollbarCol)
+		{
+			var lenDiff = scrollbarCol - rightmostCol + 1; // The amount to subtract from the length
+			itemLen -= lenDiff;
+		}
 		if (!this.borderEnabled && pStartX == this.size.width)
 			writeMenuItems = false;
 		// Just draw the whole srollbar to ensure it's updated
@@ -1509,7 +1529,7 @@ function DDLightbarMenu_GetVal(pDraw, pSelectedItemIndexes)
 			{
 				// Draw the current item in regular colors
 				this.WriteItemAtItsLocation(this.selectedItemIdx, false, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
-				--this.selectedItemIdx;
+				var oldSelectedItemIdx = this.selectedItemIdx--;
 				// Draw the new current item in selected colors
 				// If the selected item is above the top of the menu, then we'll need to
 				// scroll the items down.
@@ -1524,6 +1544,8 @@ function DDLightbarMenu_GetVal(pDraw, pSelectedItemIndexes)
 					// just draw the selected item highlighted.
 					this.WriteItemAtItsLocation(this.selectedItemIdx, true, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
 				}
+				if (typeof(this.OnItemNav) === "function")
+					this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx);
 			}
 			else
 			{
@@ -1535,6 +1557,7 @@ function DDLightbarMenu_GetVal(pDraw, pSelectedItemIndexes)
 					//this.WriteItemAtItsLocation(pIdx, pHighlight, pSelected)
 					this.WriteItemAtItsLocation(this.selectedItemIdx, false, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
 					// Go to the last item and scroll to the bottom if necessary
+					var oldSelectedItemIdx = this.selectedItemIdx;
 					this.selectedItemIdx = numItems - 1;
 					var oldTopItemIdx = this.topItemIdx;
 					var numItemsPerPage = (this.borderEnabled ? this.size.height - 2 : this.size.height);
@@ -1548,6 +1571,8 @@ function DDLightbarMenu_GetVal(pDraw, pSelectedItemIndexes)
 						// Draw the new current item in selected colors
 						this.WriteItemAtItsLocation(this.selectedItemIdx, true, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
 					}
+					if (typeof(this.OnItemNav) === "function")
+						this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx);
 				}
 			}
 		}
@@ -1560,6 +1585,7 @@ function DDLightbarMenu_GetVal(pDraw, pSelectedItemIndexes)
 			// Only do this if we're not already at the top of the list
 			if (this.topItemIdx > 0)
 			{
+				var oldSelectedItemIdx = this.selectedItemIdx;
 				var numItemsPerPage = (this.borderEnabled ? this.size.height - 2 : this.size.height);
 				var newTopItemIdx = this.topItemIdx - numItemsPerPage;
 				if (newTopItemIdx < 0)
@@ -1589,6 +1615,8 @@ function DDLightbarMenu_GetVal(pDraw, pSelectedItemIndexes)
 						this.Draw(selectedItemIndexes);
 					}
 				}
+				if (typeof(this.OnItemNav) === "function")
+					this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx);
 			}
 			else
 			{
@@ -1597,9 +1625,12 @@ function DDLightbarMenu_GetVal(pDraw, pSelectedItemIndexes)
 				// item, then make it so.
 				if (this.selectedItemIdx > 0)
 				{
+					var oldSelectedItemIdx = this.selectedItemIdx;
 					this.WriteItemAtItsLocation(this.selectedItemIdx, false, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
 					this.selectedItemIdx = 0;
 					this.WriteItemAtItsLocation(this.selectedItemIdx, true, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
+					if (typeof(this.OnItemNav) === "function")
+						this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx);
 				}
 			}
 		}
@@ -1610,6 +1641,7 @@ function DDLightbarMenu_GetVal(pDraw, pSelectedItemIndexes)
 			var lastItemIdx = this.NumItems() - 1;
 			if (lastItemIdx > this.topItemIdx+numItemsPerPage-1)
 			{
+				var oldSelectedItemIdx = this.selectedItemIdx;
 				// Figure out the top index for the last page.
 				var topIndexForLastPage = numItems - numItemsPerPage;
 				if (topIndexForLastPage < 0)
@@ -1638,6 +1670,8 @@ function DDLightbarMenu_GetVal(pDraw, pSelectedItemIndexes)
 					}
 					this.Draw(selectedItemIndexes);
 				}
+				if (typeof(this.OnItemNav) === "function")
+					this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx);
 			}
 			else
 			{
@@ -1646,9 +1680,12 @@ function DDLightbarMenu_GetVal(pDraw, pSelectedItemIndexes)
 				// item, then make it so.
 				if (this.selectedItemIdx < lastItemIdx)
 				{
+					var oldSelectedItemIdx = this.selectedItemIdx;
 					this.WriteItemAtItsLocation(this.selectedItemIdx, false, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
 					this.selectedItemIdx = lastItemIdx;
 					this.WriteItemAtItsLocation(this.selectedItemIdx, true, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
+					if (typeof(this.OnItemNav) === "function")
+						this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx);
 				}
 			}
 		}
@@ -1657,6 +1694,7 @@ function DDLightbarMenu_GetVal(pDraw, pSelectedItemIndexes)
 			// Go to the first item in the list
 			if (this.selectedItemIdx > 0)
 			{
+				var oldSelectedItemIdx = this.selectedItemIdx;
 				// If the current item index is not on first current page, then scroll.
 				// Otherwise, draw more efficiently by drawing the current item in
 				// regular colors and the first item in highlighted colors.
@@ -1685,6 +1723,8 @@ function DDLightbarMenu_GetVal(pDraw, pSelectedItemIndexes)
 						console.gotoxy(this.pos.x, this.pos.y+this.selectedItemIdx-this.topItemIdx);
 					this.WriteItem(this.selectedItemIdx, null, true, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
 				}
+				if (typeof(this.OnItemNav) === "function")
+					this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx);
 			}
 		}
 		else if (this.lastUserInput == KEY_END)
@@ -1693,6 +1733,7 @@ function DDLightbarMenu_GetVal(pDraw, pSelectedItemIndexes)
 			var numItemsPerPage = this.GetNumItemsPerPage();
 			if (this.selectedItemIdx < numItems-1)
 			{
+				var oldSelectedItemIdx = this.selectedItemIdx;
 				var lastPossibleTop = numItems - numItemsPerPage;
 				if (lastPossibleTop < 0)
 					lastPossibleTop = 0;
@@ -1726,6 +1767,8 @@ function DDLightbarMenu_GetVal(pDraw, pSelectedItemIndexes)
 						console.gotoxy(this.pos.x, this.pos.y+this.selectedItemIdx-this.topItemIdx);
 					this.WriteItem(this.selectedItemIdx, null, true, selectedItemIndexes.hasOwnProperty(this.selectedItemIdx));
 				}
+				if (typeof(this.OnItemNav) === "function")
+					this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx);
 			}
 		}
 		// Enter key or additional select-item key: Select the item & quit out of the input loop
@@ -1847,6 +1890,7 @@ function DDLightbarMenu_GetVal(pDraw, pSelectedItemIndexes)
 			// and stop the input loop.
 			if (userEnteredItemNum > 0)
 			{
+				var oldSelectedItemIdx = this.selectedItemIdx;
 				this.selectedItemIdx = userEnteredItemNum-1;
 				if (this.multiSelect)
 				{
@@ -1868,6 +1912,8 @@ function DDLightbarMenu_GetVal(pDraw, pSelectedItemIndexes)
 					retVal = this.GetItem(this.selectedItemIdx).retval;
 					continueOn = false;
 				}
+				if (typeof(this.OnItemNav) === "function")
+					this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx);
 			}
 			else
 				console.gotoxy(originalCurpos); // Move the cursor back where it was
@@ -1905,8 +1951,11 @@ function DDLightbarMenu_GetVal(pDraw, pSelectedItemIndexes)
 						else
 						{
 							retVal = theItem.retval;
+							var oldSelectedItemIdx = this.selectedItemIdx;
 							this.selectedItemIdx = i;
 							continueOn = false;
+							if (typeof(this.OnItemNav) === "function")
+								this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx);
 						}
 						break;
 					}
@@ -1946,7 +1995,7 @@ function DDLightbarMenu_DoKeyDown(pSelectedItemIndexes, pNumItems)
 	{
 		// Draw the current item in regular colors
 		this.WriteItemAtItsLocation(this.selectedItemIdx, false, selectedItemIndexes.hasOwnProperty(+(this.selectedItemIdx)));
-		++this.selectedItemIdx;
+		var oldSelectedItemIdx = this.selectedItemIdx++;
 		// Draw the new current item in selected colors
 		// If the selected item is below the bottom of the menu, then we'll need to
 		// scroll the items up.
@@ -1962,6 +2011,8 @@ function DDLightbarMenu_DoKeyDown(pSelectedItemIndexes, pNumItems)
 			// just draw the selected item highlighted.
 			this.WriteItemAtItsLocation(this.selectedItemIdx, true, selectedItemIndexes.hasOwnProperty(+(this.selectedItemIdx)));
 		}
+		if (typeof(this.OnItemNav) === "function")
+			this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx);
 	}
 	else
 	{
@@ -1972,6 +2023,7 @@ function DDLightbarMenu_DoKeyDown(pSelectedItemIndexes, pNumItems)
 			// Draw the current item in regular colors
 			this.WriteItemAtItsLocation(this.selectedItemIdx, false, selectedItemIndexes.hasOwnProperty(+(this.selectedItemIdx)));
 			// Go to the first item and scroll to the top if necessary
+			var oldSelectedItemIdx = this.selectedItemIdx;
 			this.selectedItemIdx = 0;
 			var oldTopItemIdx = this.topItemIdx;
 			this.topItemIdx = 0;
@@ -1982,6 +2034,8 @@ function DDLightbarMenu_DoKeyDown(pSelectedItemIndexes, pNumItems)
 				// Draw the new current item in selected colors
 				this.WriteItemAtItsLocation(this.selectedItemIdx, true, selectedItemIndexes.hasOwnProperty(+(this.selectedItemIdx)));
 			}
+			if (typeof(this.OnItemNav) === "function")
+				this.OnItemNav(oldSelectedItemIdx, this.selectedItemIdx);
 		}
 	}
 }
@@ -2449,6 +2503,79 @@ function DDLightbarMenu_SetSelectedItemIdx(pSelectedItemIdx)
 		this.topItemIdx = this.selectedItemIdx;
 }
 
+// Gets the index of the bottommost item on the menu
+function DDLightbarMenu_GetBottomItemIdx()
+{
+	var bottomItemIdx = this.topItemIdx + this.size.height - 1;
+	if (this.borderEnabled)
+		bottomItemIdx -= 2;
+	return bottomItemIdx;
+}
+
+// Returns the absolute screen position (x, y) of the topmost displayed item on the menu
+//
+// Return value: An object with the following properties:
+//               x: The horizontal screen location of the top item (1-based)
+//               y: The vertical screen location of the top item (1-based)
+function DDLightbarMenu_GetTopDisplayedItemPos()
+{
+	var itemPos = {
+		x: this.pos.x,
+		y: this.pos.y
+	};
+	if (this.borderEnabled)
+	{
+		++itemPos.x;
+		++itemPos.y;
+	}
+	return itemPos;
+}
+
+// Returns the absolute screen position (x, y) of the bottommost displayed item on the menu
+//
+// Return value: An object with the following properties:
+//               x: The horizontal screen location of the top item (1-based)
+//               y: The vertical screen location of the top item (1-based)
+function DDLightbarMenu_GetBottomDisplayedItemPos()
+{
+	var itemPos = {
+		x: this.pos.x,
+		y: this.pos.y + this.size.height - 1
+	};
+	if (this.borderEnabled)
+	{
+		++itemPos.x;
+		--itemPos.y;
+	}
+	return itemPos;
+}
+
+// Returns the absolute screen row number for an item index, if it is visible
+// on the menu.  If the item is not visible on the menu, this will return -1.
+//
+// Parameters:
+//  pItemIdx: The index of the menu item to check
+//
+// Return value: The absolute row number on the screen where the item is, if it is
+//               visible on the menu, or -1 if the item is not visible on the menu.
+function DDLightbarMenu_ScreenRowForItem(pItemIdx)
+{
+	if (typeof(pItemIdx) !== "number")
+		return -1;
+	if (pItemIdx < 0 || pItemIdx >= this.NumItems())
+		return -1;
+
+	var screenRow = -1;
+	if (pItemIdx >= this.topItemIdx && pItemIdx <= this.GetBottomItemIdx())
+	{
+		if (this.borderEnabled)
+			screenRow = this.pos.y + pItemIdx - this.topItemIdx + 1;
+		else
+			screenRow = this.pos.y + pItemIdx - this.topItemIdx;
+	}
+	return screenRow;
+}
+
 // Calculates the number of solid scrollbar blocks & non-solid scrollbar blocks
 // to use.  Saves the information in this.scrollbarInfo.numSolidScrollBlocks and
 // this.scrollbarInfo.numNonSolidScrollBlocks.
diff --git a/xtrn/ddfilelister/ddfilelister.js b/xtrn/ddfilelister/ddfilelister.js
index a1a9e101403b58730f1c2499aba833e8b72662de..c54a109bdd5ef8e92ac53c1c423066ea6a3f1c08 100644
--- a/xtrn/ddfilelister/ddfilelister.js
+++ b/xtrn/ddfilelister/ddfilelister.js
@@ -30,6 +30,15 @@
  * 2022-03-09 Eric Oulashin     Version 2.04
  *                              Bug fix: Now successfully formats filenames without extensions
  *                              when listing files.
+ * 2022-03-12 Eric Oulashin     Version 2.05
+ *                              Now makes use of the user's extended file description setting:
+ *                              If the user's extended file description setting is enabled,
+ *                              the lister will now show extended file descriptions on the
+ *                              main screen in a split format, with the lightbar file list
+ *                              on the left and the extended file description for the
+ *                              highlighted file on the right.  Also, made the file info
+ *                              window taller for terminals within 25 lines high.
+ *                              I had started work on this on March 9, 2022.
 */
 
 if (typeof(require) === "function")
@@ -40,6 +49,7 @@ if (typeof(require) === "function")
 	require("frame.js", "Frame");
 	require("scrollbar.js", "ScrollBar");
 	require("mouse_getkey.js", "mouse_getkey");
+	require("attr_conv.js", "convertAttrsToSyncPerSysCfg");
 }
 else
 {
@@ -49,6 +59,7 @@ else
 	load("frame.js");
 	load("scrollbar.js");
 	load("mouse_getkey.js");
+	load("attr_conv.js");
 }
 
 
@@ -83,8 +94,8 @@ if (system.version_num < 31900)
 }
 
 // Lister version information
-var LISTER_VERSION = "2.04";
-var LISTER_DATE = "2022-03-09";
+var LISTER_VERSION = "2.05";
+var LISTER_DATE = "2022-03-12";
 
 
 ///////////////////////////////////////////////////////////////////////////////
@@ -114,10 +125,6 @@ var gListIdxes = {
 // The end index of each column includes the trailing space so that
 // highlight colors will highlight the whole field
 gListIdxes.filenameEnd = gListIdxes.filenameStart + 13;
-// For terminals that are at least 100 characters wide, allow 10 more characters
-// for the filename.  This will also give more space for the description.
-if (console.screen_columns >= 100)
-	gListIdxes.filenameEnd += 10;
 gListIdxes.fileSizeStart = gListIdxes.filenameEnd;
 gListIdxes.fileSizeEnd = gListIdxes.fileSizeStart + 7;
 gListIdxes.descriptionStart = gListIdxes.fileSizeEnd;
@@ -208,7 +215,10 @@ parseArgs(argv);
 // This array will contain file metadata objects
 var gFileList = [];
 
-// Populate the file list based on the script mode (list/search)
+// Populate the file list based on the script mode (list/search).
+// It's important that this is called before createFileListMenu(),
+// since this adjusts gListIdxes.filenameEnd based on the longest
+// filename length and terminal width.
 var listPopRetObj = populateFileList(gScriptMode);
 if (listPopRetObj.exitNow)
 	exit(listPopRetObj.exitCode);
@@ -240,6 +250,9 @@ var gFileListMenu = createFileListMenu(fileMenuBar.getAllActionKeysStr(true, tru
 // In a loop, show the file list menu, allowing the user to scroll the file list,
 // and respond to user input until the user decides to quit.
 gFileListMenu.Draw({});
+// If using extended descriptions, write the first file's description on the screen
+if (extendedDescEnabled())
+	displayFileExtDescOnMainScreen(0);
 var continueDoingFileList = true;
 var drawFileListMenu = false; // For screen refresh optimization
 while (continueDoingFileList)
@@ -248,6 +261,7 @@ while (continueDoingFileList)
 	for (var prop in gFileListMenu.selectedItemIndexes)
 		delete gFileListMenu.selectedItemIndexes[prop];
 	var actionRetObj = null;
+	var currentActionVal = null;
 	var userChoice = gFileListMenu.GetVal(drawFileListMenu, gFileListMenu.selectedItemIndexes);
 	drawFileListMenu = false; // For screen refresh optimization
 	var lastUserInputUpper = gFileListMenu.lastUserInput != null ? gFileListMenu.lastUserInput.toUpperCase() : null;
@@ -259,7 +273,7 @@ while (continueDoingFileList)
 		fileMenuBar.incrementMenuItemAndRefresh();
 	else if (lastUserInputUpper == KEY_ENTER)
 	{
-		var currentActionVal = fileMenuBar.getCurrentSelectedAction();
+		currentActionVal = fileMenuBar.getCurrentSelectedAction();
 		fileMenuBar.setCurrentActionCode(currentActionVal);
 		actionRetObj = doAction(currentActionVal, gFileList, gFileListMenu);
 	}
@@ -270,11 +284,12 @@ while (continueDoingFileList)
 		{
 			fileMenuBar.setCurrentActionCode(FILE_DELETE, true);
 			actionRetObj = doAction(FILE_DELETE, gFileList, gFileListMenu);
+			currentActionVal = FILE_DELETE;
 		}
 	}
 	else
 	{
-		var currentActionVal = fileMenuBar.getActionFromChar(lastUserInputUpper, false);
+		currentActionVal = fileMenuBar.getActionFromChar(lastUserInputUpper, false);
 		fileMenuBar.setCurrentActionCode(currentActionVal, true);
 		actionRetObj = doAction(currentActionVal, gFileList, gFileListMenu);
 	}
@@ -300,47 +315,87 @@ while (continueDoingFileList)
 			if (actionRetObj.reDrawCmdBar) // Could call fileMenuBar.constructPromptText(); if needed
 				fileMenuBar.writePromptLine();
 			var redrewPartOfFileListMenu = false;
-			if (actionRetObj.fileListPartialRedrawInfo != null)
+			// If we are to re-draw the main screen content, then
+			// enable the flag to draw the file list menu on the next
+			// GetVal(); also, if extended descriptions are being shown,
+			// write the current file's extended description too.
+			if (actionRetObj.reDrawMainScreenContent)
 			{
-				drawFileListMenu = false;
-				var startX = actionRetObj.fileListPartialRedrawInfo.startX;
-				var startY = actionRetObj.fileListPartialRedrawInfo.startY;
-				var width = actionRetObj.fileListPartialRedrawInfo.width;
-				var height = actionRetObj.fileListPartialRedrawInfo.height;
-				gFileListMenu.DrawPartial(startX, startY, width, height, {});
-				redrewPartOfFileListMenu = true;
+				drawFileListMenu = true;
+				if (extendedDescEnabled())
+					displayFileExtDescOnMainScreen(gFileListMenu.selectedItemIdx);
 			}
 			else
 			{
-				continueDoingFileList = actionRetObj.continueFileLister;
-				drawFileListMenu = actionRetObj.reDrawFileListMenu;
-			}
-			// If we're not redrawing the whole file list menu, then remove
-			// checkmarks from any selected files
-			if (!drawFileListMenu && gFileListMenu.numSelectedItemIndexes() > 0)
-			{
-				var lastItemIdxOnScreen = gFileListMenu.topItemIdx + gFileListMenu.size.height - 1;
-				var listItemStartRow = gFileListMenu.pos.y;
-				var redrawStartX = gFileListMenu.pos.x + gFileListMenu.size.width - 1;
-				var redrawWidth = 1;
-				if (gFileListMenu.borderEnabled) // Shouldn't have this enabled
+				// If there is partial redraw information available, then use it
+				// to re-draw that part of the main screen
+				if (actionRetObj.fileListPartialRedrawInfo != null)
+				{
+					drawFileListMenu = false;
+					var startX = actionRetObj.fileListPartialRedrawInfo.absStartX;
+					var startY = actionRetObj.fileListPartialRedrawInfo.absStartY;
+					var width = actionRetObj.fileListPartialRedrawInfo.width;
+					var height = actionRetObj.fileListPartialRedrawInfo.height;
+					refreshScreenMainContent(startX, startY, width, height, true);
+					actionRetObj.refreshedSelectedFilesAlready = true;
+					redrewPartOfFileListMenu = true;
+				}
+				else
 				{
-					--lastItemIdxOnScreen;
-					++listItemStartRow;
-					--redrawStartX;
+					// Partial screen re-draw information was not returned.
+					continueDoingFileList = actionRetObj.continueFileLister;
+					drawFileListMenu = actionRetObj.reDrawMainScreenContent;
+					// If displaying extended descriptions and the user deleted some files, then
+					// refresh the file description area to erase the delete confirmation text
+					if (extendedDescEnabled()/* && currentActionVal == FILE_DELETE*/)
+					{
+						if (actionRetObj.hasOwnProperty("filesDeleted") && actionRetObj.filesDeleted)
+						{
+							var numFiles = gFileListMenu.NumItems();
+							if (numFiles > 0 && gFileListMenu.selectedItemIdx >= 0 && gFileListMenu.selectedItemIdx < numFiles)
+								displayFileExtDescOnMainScreen(gFileListMenu.selectedItemIdx);
+						}
+						else
+						{
+							var firstLine = startY + gFileListMenu.pos.y;
+							var lastLine = console.screen_rows - 1;
+							var width = console.screen_columns - gFileListMenu.size.width - 1;
+							displayFileExtDescOnMainScreen(gFileListMenu.selectedItemIdx, firstLine, lastLine, width);
+						}
+					}
 				}
-				if (gFileListMenu.scrollbarEnabled && !gFileListMenu.CanShowAllItemsInWindow())
+			}
+			// Remove checkmarks from any selected files in the file menu.
+			// For efficiency, we'd probably only do this if not re-drawing the wohle
+			// menu, but that's not working for now.
+			if (!actionRetObj.refreshedSelectedFilesAlready && /*!drawFileListMenu &&*/ gFileListMenu.numSelectedItemIndexes() > 0)
+			{
+				var bottomItemIdx = gFileListMenu.GetBottomItemIdx();
+				var redrawTopY = -1;
+				var redrawBottomY = -1;
+				if (actionRetObj.fileListPartialRedrawInfo != null)
 				{
-					--redrawStartX;
-					++redrawWidth;
+					redrawTopY = actionRetObj.fileListPartialRedrawInfo.absStartY;
+					redrawBottomY = actionRetObj.fileListPartialRedrawInfo.height + height - 1;
 				}
 				for (var idx in gFileListMenu.selectedItemIndexes)
 				{
 					var idxNum = +idx;
-					if (idxNum >= gFileListMenu.topItemIdx && idxNum <= lastItemIdxOnScreen)
+					if (idxNum >= gFileListMenu.topItemIdx && idxNum <= bottomItemIdx)
 					{
-						gFileListMenu.DrawPartialAbs(redrawStartX, listItemStartRow+idxNum, redrawWidth, 1, {});
-						redrewPartOfFileListMenu = true;
+						var drawItem = true;
+						if (redrawTopY > -1 && redrawBottomY > redrawTopY)
+						{
+							var screenRowForItem = gFileListMenu.ScreenRowForItem(idxNum);
+							drawItem = (screenRowForItem < redrawTopY || screenRowForItem > redrawBottomY)
+						}
+						if (drawItem)
+						{
+							var isSelected = (idxNum == gFileListMenu.selectedItemIdx);
+							gFileListMenu.WriteItemAtItsLocation(idxNum, isSelected, false);
+						}
+						else
+							console.print("\1n\r\nNot drawing idx " + idxNum + "\r\n\1p");
 					}
 				}
 			}
@@ -410,7 +465,8 @@ function doAction(pActionCode, pFileList, pFileListMenu)
 //
 // Return value: An object with the following properties:
 //               continueFileLister: Boolean - Whether or not the file lister should continue, or exit
-//               reDrawFileListMenu: Boolean - Whether or not to re-draw the whole file list
+//               reDrawMainScreenContent: Boolean - Whether or not to re-draw the main screen content
+//                                        (file list, and extended description area if applicable)
 //               reDrawListerHeader: Boolean - Whether or not to re-draw the header at the top of the screen
 //               reDrawHeaderTextOnly: Boolean - Whether or not to re-draw the header text only.  This should
 //                                     take precedence over reDrawListerHeader.
@@ -421,17 +477,20 @@ function doAction(pActionCode, pFileList, pFileListMenu)
 //                                          startY: The starting Y coordinate for where to re-draw
 //                                          width: The width to re-draw
 //                                          height: The height to re-draw
+//               refreshedSelectedFilesAlready: Whether or not selected file checkmark items
+//                                              have already been refreshed (boolean)
 //               exitNow: Exit the file lister now (boolean)
 //               If no part of the file list menu needs to be re-drawn, this will be null.
 function getDefaultActionRetObj()
 {
 	return {
 		continueFileLister: true,
-		reDrawFileListMenu: false,
+		reDrawMainScreenContent: false,
 		reDrawListerHeader: false,
 		reDrawHeaderTextOnly: false,
 		reDrawCmdBar: false,
 		fileListPartialRedrawInfo: null,
+		refreshedSelectedFilesAlready: false,
 		exitNow: false
 	};
 }
@@ -450,8 +509,8 @@ function showFileInfo(pFileList, pFileListMenu)
 
 	// The width of the frame to display the file info (including borders).  This
 	// is declared early so that it can be used for string length adjustment.
-	var frameWidth = pFileListMenu.size.width - 4;
-	var frameInnerWidth = frameWidth - 2; // Without borders
+	//var frameWidth = pFileListMenu.size.width - 4; // TODO: Remove?
+	var frameWidth = console.screen_columns - 4;
 
 	// pFileList[pFileListMenu.selectedItemIdx] has a file metadata object without
 	// extended information.  Get a metadata object with extended information so we
@@ -461,26 +520,24 @@ function showFileInfo(pFileList, pFileListMenu)
 	var dirCode = bbs.curdir_code;
 	if (pFileList[pFileListMenu.selectedItemIdx].hasOwnProperty("dirCode"))
 		dirCode = pFileList[pFileListMenu.selectedItemIdx].dirCode;
-	var fileInfoObj = getFileInfoFromFilebase(dirCode, pFileList[pFileListMenu.selectedItemIdx].name, FileBase.DETAIL.EXTENDED);
-	var extdFileInfo = fileInfoObj.fileMetadataObj;
-	if (typeof(extdFileInfo) !== "object")
-	{
-		displayMsg("Unable to get file info!", true, true);
-		return;
-	}
-	var fileTime = fileInfoObj.fileTime;
+	var fileMetadata = null;
+	if (extendedDescEnabled())
+		fileMetadata = pFileList[pFileListMenu.selectedItemIdx];
+	else
+		fileMetadata = getFileInfoFromFilebase(dirCode, pFileList[pFileListMenu.selectedItemIdx].name, FileBase.DETAIL.EXTENDED);
 	// Build a string with the file information
 	// Make sure the displayed filename isn't too crazy long
-	var adjustedFilename = shortenFilename(extdFileInfo.name, frameInnerWidth, false);
+	var frameInnerWidth = frameWidth - 2; // Without borders
+	var adjustedFilename = shortenFilename(fileMetadata.name, frameInnerWidth, false);
 	var fileInfoStr = "\1n\1wFilename";
-	if (adjustedFilename.length < extdFileInfo.name.length)
+	if (adjustedFilename.length < fileMetadata.name.length)
 		fileInfoStr += " (shortened)";
 	fileInfoStr += ":\r\n";
 	fileInfoStr += gColors.filename + adjustedFilename +  "\1n\1w\r\n";
-	// Note: File size can also be retrieved by calling a FileBase's get_size(extdFileInfo.name)
+	// Note: File size can also be retrieved by calling a FileBase's get_size(fileMetadata.name)
 	// TODO: Shouldn't need the max length here
-	fileInfoStr += "Size: " + gColors.fileSize + getFileSizeStr(extdFileInfo.size, 99999) + "\1n\1w\r\n";
-	fileInfoStr += "Timestamp: " + gColors.fileTimestamp + strftime("%Y-%m-%d %H:%M:%S", fileTime) + "\1n\1w\r\n"
+	fileInfoStr += "Size: " + gColors.fileSize + getFileSizeStr(fileMetadata.size, 99999) + "\1n\1w\r\n";
+	fileInfoStr += "Timestamp: " + gColors.fileTimestamp + strftime("%Y-%m-%d %H:%M:%S", fileMetadata.time) + "\1n\1w\r\n"
 	fileInfoStr += "\r\n";
 
 	// File library/directory information
@@ -492,37 +549,50 @@ function showFileInfo(pFileList, pFileListMenu)
 	fileInfoStr += "\1c\1hDir\1g: \1n\1c" + dirDesc.substr(0, frameInnerWidth-5) + "\1n\1w\r\n";
 	fileInfoStr += "\r\n";
 
-	// extdFileInfo should have extdDesc, but check just in case
+	// fileMetadata should have extdDesc, but check just in case
 	var fileDesc = "";
-	if (extdFileInfo.hasOwnProperty("extdesc") && extdFileInfo.extdesc.length > 0)
-		fileDesc = extdFileInfo.extdesc;
+	if (fileMetadata.hasOwnProperty("extdesc") && fileMetadata.extdesc.length > 0)
+		fileDesc = fileMetadata.extdesc;
 	else
-		fileDesc = extdFileInfo.desc;
+		fileDesc = fileMetadata.desc;
 	// It's possible for fileDesc to be undefined (due to extDesc or desc being undefined),
 	// so make sure it's a string
 	if (typeof(fileDesc) !== "string")
 		fileDesc = "";
+	// This might be overkill, but just in case, convert any non-Synchronet
+	// attribute codes to Synchronet attribute codes in the description.
+	if (!fileMetadata.hasOwnProperty("attrsConverted"))
+	{
+		fileDesc = convertAttrsToSyncPerSysCfg(fileDesc);
+		fileMetadata.attrsConverted = true;
+		if (fileMetadata.hasOwnProperty("extdesc"))
+			fileMetadata.extdesc = fileDesc;
+		else
+			fileMetadata.desc = fileDesc;
+	}
+
 	fileInfoStr += gColors.desc;
 	if (fileDesc.length > 0)
 		fileInfoStr += "Description:\r\n" + fileDesc;
 	else
 		fileInfoStr += "No description available";
+	fileInfoStr += "\r\n";
 	if (user.is_sysop)
 	{
 		var sysopFields = [ "from", "cost", "added"];
 		for (var sI = 0; sI < sysopFields.length; ++sI)
 		{
 			var prop = sysopFields[sI];
-			if (extdFileInfo.hasOwnProperty(prop))
+			if (fileMetadata.hasOwnProperty(prop))
 			{
-				if (typeof(extdFileInfo[prop]) === "string" && extdFileInfo[prop].length == 0)
+				if (typeof(fileMetadata[prop]) === "string" && fileMetadata[prop].length == 0)
 					continue;
 				var propName = prop.charAt(0).toUpperCase() + prop.substr(1);
 				fileInfoStr += "\r\n\1n\1c\1h" + propName + "\1g:\1n\1c ";
 				if (prop == "added")
-					fileInfoStr += strftime("%Y-%m-%d %H:%M:%S", extdFileInfo.added);
+					fileInfoStr += strftime("%Y-%m-%d %H:%M:%S", fileMetadata.added);
 				else
-					fileInfoStr += extdFileInfo[prop].toString().substr(0, frameInnerWidth);
+					fileInfoStr += fileMetadata[prop].toString().substr(0, frameInnerWidth);
 				fileInfoStr += "\1n\1w";
 			}
 		}
@@ -531,15 +601,15 @@ function showFileInfo(pFileList, pFileListMenu)
 
 	// Construct & draw a frame with the file information & do the input loop
 	// for the frame until the user closes the frame.
-	var frameUpperLeftX = pFileListMenu.pos.x + 2;
-	var frameUpperLeftY = pFileListMenu.pos.y + 2;
+	var frameUpperLeftX = 3;
+	var frameUpperLeftY = gNumHeaderLinesDisplayed + 3;
 	// Note: frameWidth is declared earlier
-	var frameHeight = 10;
+	var frameHeight = console.screen_rows - 4 - frameUpperLeftY;
 	// If the user's console is more than 25 rows high, then make the info window
 	// taller so that its bottom row is 10 from the bottom, but only up to 45 rows tall.
 	if (console.screen_rows > 25)
 	{
-		var frameBottomRow = console.screen_rows - 10;
+		var frameBottomRow = console.screen_rows - 4;
 		frameHeight = frameBottomRow - frameUpperLeftY + 1;
 		if (frameHeight > 45)
 			frameHeight = 45;
@@ -552,9 +622,11 @@ function showFileInfo(pFileList, pFileListMenu)
 	// Construct the file list redraw info.  Note that the X and Y are relative
 	// to the file list menu, not absolute screen coordinates.
 	retObj.fileListPartialRedrawInfo = {
-		startX: 2,
-		startY: 2,
-		width: frameWidth+1,
+		startX: frameUpperLeftX - gFileListMenu.pos.x + 1, // Relative to the file menu
+		startY: frameUpperLeftY - gFileListMenu.pos.y + 1, // Relative to the file menu
+		absStartX: frameUpperLeftX,
+		absStartY: frameUpperLeftY,
+		width: frameWidth,
 		height: frameHeight
 	};
 
@@ -597,7 +669,7 @@ function viewFile(pFileList, pFileListMenu)
 		console.pause();
 
 	retObj.reDrawListerHeader = true;
-	retObj.reDrawFileListMenu = true;
+	retObj.reDrawMainScreenContent = true;
 	retObj.reDrawCmdBar = true;
 	return retObj;
 }
@@ -635,6 +707,7 @@ function addSelectedFilesToBatchDLQueue(pFileList, pFileListMenu)
 	// Note that confirmFileActionWithUser() will re-draw the parts of the file
 	// list menu that are necessary.
 	var addFilesConfirmed = confirmFileActionWithUser(filenames, "Batch DL add", false);
+	retObj.refreshedSelectedFilesAlready = true;
 	if (addFilesConfirmed)
 	{
 		var batchDLQueueStats = getUserDLQueueStats();
@@ -680,19 +753,9 @@ function addSelectedFilesToBatchDLQueue(pFileList, pFileListMenu)
 		// Frame location & size for batch DL queue stats or filenames that failed
 		var frameUpperLeftX = gFileListMenu.pos.x + 2;
 		var frameUpperLeftY = gFileListMenu.pos.y + 2;
-		var frameWidth = gFileListMenu.size.width - 4;
+		var frameWidth = console.screen_columns - 4; // Used to be gFileListMenu.size.width - 4;
 		var frameInnerWidth = frameWidth - 2; // Without borders
 		var frameHeight = 8;
-		// To make the list refresh info to return to the main script loop
-		function makeBatchRefreshInfoObj(pFrameWidth, pFrameHeight)
-		{
-			return {
-				startX: 3,
-				startY: 3,
-				width: pFrameWidth+1,
-				height: pFrameHeight
-			};
-		}
 
 		// If there were no failures, then show a success message & prompt the user if they
 		// want to download their batch queue.  Otherwise, show the filenames that failed to
@@ -745,22 +808,23 @@ function addSelectedFilesToBatchDLQueue(pFileList, pFileListMenu)
 				                                                       frameHeight, gColors.batchDLInfoWindowBorder,
 				                                                       frameTitle, gColors.batchDLInfoWindowTitle,
 				                                                       queueStats, additionalQuitKeys);
+				// The main screen content (file list & extended description if applicable)
+				// will need to be redrawn after this.
+				retObj.reDrawMainScreenContent = true;
+				// If the user chose to download their file queue, then send it to the user.
+				// And the lister headers will need to be re-drawn as well.
 				if (lastUserInput.toUpperCase() == "Y")
 				{
-					retObj.reDrawFileListMenu = true;
 					retObj.reDrawListerHeader = true;
 					retObj.reDrawCmdBar = true;
 					console.print("\1n");
 					console.gotoxy(1, console.screen_rows);
 					console.crlf();
 					bbs.batch_download();
-				}
-				else
-				{
-					retObj.reDrawFileListMenu = true;
-					// Construct the file list redraw info.  Note that the X and Y are relative
-					// to the file list menu, not absolute screen coordinates.
-					//retObj.fileListPartialRedrawInfo = makeBatchRefreshInfoObj(frameWidth, frameHeight);
+					// If the user is still online (chose not to hang up after transfer),
+					// then pause so that the user can see the batch download status
+					if (bbs.online > 0)
+						console.pause();
 				}
 			}
 		}
@@ -777,9 +841,17 @@ function addSelectedFilesToBatchDLQueue(pFileList, pFileListMenu)
 																   frameHeight, gColors.batchDLInfoWindowBorder,
 																   frameTitle, gColors.batchDLInfoWindowTitle,
 																   fileListStr, "");
-			// Construct the file list redraw info.  Note that the X and Y are relative
+			// Add the file list redraw info.  Note that the X and Y are relative
 			// to the file list menu, not absolute screen coordinates.
-			retObj.fileListPartialRedrawInfo = makeBatchRefreshInfoObj(frameWidth, frameHeight);
+			// To make the list refresh info to return to the main script loop
+			retObj.fileListPartialRedrawInfo = {
+				startX: 3,
+				startY: 3,
+				absStartX: gFileListMenu.pos.x + 3 - 1, // 1-based
+				absStartY: gFileListMenu.pos.y + 3 - 1, // 1-based
+				width: frameWidth + 1,
+				height: frameHeight
+			};
 		}
 	}
 
@@ -836,33 +908,6 @@ function getUserDLQueueStats()
 				}
 			}
 		}
-
-		/*
-		var sections = batchDLFile.iniGetSections();
-		retObj.numFilesInQueue = sections.length;
-		for (var i = 0; i < sections.length; ++i)
-		{
-			//var desc = 
-			//retObj.filenames.push({ filename: sections[i], desc:  });
-			// Get the dir code from the section, then get the size and cost for
-			// the file from the filebase and add them to the totals in retObj
-			var dirCode = batchDLFile.iniGetValue(sections[i], "dir", "");
-			if (dirCode.length > 0)
-			{
-				var filebase = new FileBase(dirCode);
-				if (filebase.open())
-				{
-					var fileInfo = filebase.get(sections[i]);
-					if (typeof(fileInfo) === "object")
-					{
-						retObj.totalSize += +(fileInfo.size);
-						retObj.totalCost += +(fileInfo.cost);
-					}
-					filebase.close();
-				}
-			}
-		}
-		*/
 		batchDLFile.close();
 	}
 
@@ -934,7 +979,7 @@ function displayHelpScreen()
 	//console.pause();
 
 	retObj.reDrawListerHeader = true;
-	retObj.reDrawFileListMenu = true;
+	retObj.reDrawMainScreenContent = true;
 	retObj.reDrawCmdBar = true;
 	return retObj;
 }
@@ -964,6 +1009,7 @@ function chooseFilebaseAndMoveFileToOtherFilebase(pFileList, pFileListMenu)
 	// Note that confirmFileActionWithUser() will re-draw the parts of the file
 	// list menu that are necessary.
 	var moveFilesConfirmed = confirmFileActionWithUser(filenames, "Move", false);
+	retObj.refreshedSelectedFilesAlready = true;
 	if (!moveFilesConfirmed)
 		return retObj;
 
@@ -976,6 +1022,8 @@ function chooseFilebaseAndMoveFileToOtherFilebase(pFileList, pFileListMenu)
 	var fileListPartialRedrawInfo = {
 		startX: fileLibMenu.pos.x - pFileListMenu.pos.x + 1,
 		startY: topYForRefresh - pFileListMenu.pos.y + 1,
+		absStartX: fileLibMenu.pos.x,
+		absStartY: topYForRefresh,
 		width: fileLibMenu.size.width + 1,
 		height: fileLibMenu.size.height + 1 // + 1 because of the label above the menu
 	};
@@ -1061,39 +1109,13 @@ function chooseFilebaseAndMoveFileToOtherFilebase(pFileList, pFileListMenu)
 					// Have the file list menu set up its description width, colors, and format
 					// string again in case it no longer needs to use its scrollbar
 					pFileListMenu.SetItemWidthsColorsAndFormatStr();
-					retObj.reDrawFileListMenu = true;
+					retObj.reDrawMainScreenContent = true;
 				}
 				else
 				{
 					// Note: getFileInfoFromFilebase() will add dirCode to the metadata object
-					var fileDataObj = getFileInfoFromFilebase(chosenDirCode, pFileList[fileIdx].name, FileBase.DETAIL.NORM);
-					pFileList[fileIdx] = fileDataObj.fileMetadataObj;
-					/*
-					// If all files were in the same directory, then we'll need to update the header
-					// lines at the top of the file list.  If there's only one file in the list,
-					// the header lines will need to display the correct directory.  Otherwise,
-					// set allSameDir to false so the header lines will now say "various".
-					// However, if not all files were in the same directory, check to see if they
-					// are now, and if so, we'll need to re-draw the header lines.
-					if (typeof(pFileList.allSameDir) == "boolean")
-					{
-						if (pFileList.allSameDir)
-						{
-							if (pFileList.length > 1)
-								pFileList.allSameDir = false;
-							//retObj.reDrawListerHeader = true;
-							retObj.reDrawHeaderTextOnly = true;
-						}
-						else
-						{
-							pFileList.allSameDir = true; // Until we find it's not true
-							for (var fileListIdx = 1; fileListIdx < pFileList.length && pFileList.allSameDir; ++fileListIdx)
-								pFileList.allSameDir = (pFileList[fileListIdx].dirCode == pFileList[0].dirCode);
-							//retObj.reDrawListerHeader =  pFileList.allSameDir;
-							retObj.reDrawHeaderTextOnly =  pFileList.allSameDir;
-						}
-					}
-					*/
+					var fileDetail = (extendedDescEnabled() ? FileBase.DETAIL.EXTENDED : FileBase.DETAIL.NORM);
+					pFileList[fileIdx] = getFileInfoFromFilebase(chosenDirCode, pFileList[fileIdx].name, fileDetail);
 				}
 			}
 			else
@@ -1124,7 +1146,6 @@ function chooseFilebaseAndMoveFileToOtherFilebase(pFileList, pFileListMenu)
 			{
 				if (pFileList.length > 1)
 					pFileList.allSameDir = false;
-				//retObj.reDrawListerHeader = true;
 				retObj.reDrawHeaderTextOnly = true;
 			}
 			else
@@ -1132,7 +1153,6 @@ function chooseFilebaseAndMoveFileToOtherFilebase(pFileList, pFileListMenu)
 				pFileList.allSameDir = true; // Until we find it's not true
 				for (var fileListIdx = 1; fileListIdx < pFileList.length && pFileList.allSameDir; ++fileListIdx)
 					pFileList.allSameDir = (pFileList[fileListIdx].dirCode == pFileList[0].dirCode);
-				//retObj.reDrawListerHeader =  pFileList.allSameDir;
 				retObj.reDrawHeaderTextOnly =  pFileList.allSameDir;
 			}
 		}
@@ -1158,17 +1178,12 @@ function chooseFilebaseAndMoveFileToOtherFilebase(pFileList, pFileListMenu)
 			displayMsg("There are no more files.", false);
 			retObj.exitNow = true;
 		}
-		// If not exiting now, we'll want to re-draw part of the file list to erase the
-		// area chooser menu.
-		if (!retObj.exitNow)
-			retObj.fileListPartialRedrawInfo = fileListPartialRedrawInfo;
 	}
-	else
-	{
-		// The user has canceled out of the area selection.
-		// We'll want to re-draw part of the file list to erase the area chooser menu.
+
+	// If not exiting now, we'll want to re-draw part of the file list to erase the
+	// area chooser menu.
+	if (!retObj.exitNow)
 		retObj.fileListPartialRedrawInfo = fileListPartialRedrawInfo;
-	}
 
 	return retObj;
 }
@@ -1180,10 +1195,14 @@ function chooseFilebaseAndMoveFileToOtherFilebase(pFileList, pFileListMenu)
 //  pFileListMenu: The menu object for the file diretory
 //
 // Return value: An object with values to indicate status & screen refresh actions; see
-//               getDefaultActionRetObj() for details.
+//               getDefaultActionRetObj() for details.  For this function, the object
+//               returned will have the following additional properties:
+//               filesDeleted: Boolean - Whether or not files were actually deleted (after
+//                             confirmation)
 function confirmAndRemoveFilesFromFilebase(pFileList, pFileListMenu)
 {
 	var retObj = getDefaultActionRetObj();
+	retObj.filesDeleted = false;
 
 	// Confirm the action with the user.  If the user confirms, then remove the file(s).
 	// If there are multiple selected files, then prompt to remove each of them.
@@ -1199,8 +1218,11 @@ function confirmAndRemoveFilesFromFilebase(pFileList, pFileListMenu)
 	// Note that confirmFileActionWithUser() will re-draw the parts of the file list menu
 	// that are necessary.
 	var removeFilesConfirmed = confirmFileActionWithUser(filenames, "Remove", false);
+	retObj.refreshedSelectedFilesAlready = true;
 	if (removeFilesConfirmed)
 	{
+		retObj.filesDeleted = true; // Assume true even if some deletions may fail
+
 		var fileIndexes = [];
 		if (pFileListMenu.numSelectedItemIndexes() > 0)
 		{
@@ -1238,10 +1260,36 @@ function confirmAndRemoveFilesFromFilebase(pFileList, pFileListMenu)
 			var filebase = new FileBase(pFileList[fileIdx].dirCode);
 			if (filebase.open())
 			{
-				// FileBase.remove(filename [,delete=false])
-				removeFileSucceeded = filebase.remove(pFileList[fileIdx].name, true);
-				if (gScriptMode == MODE_LIST_CURDIR)
-					numFilesRemaining = filebase.files;
+				var filenameFullPath = filebase.get_path(pFileList[fileIdx].name); // For logging
+				try
+				{
+					removeFileSucceeded = filebase.remove(pFileList[fileIdx].name, true);
+				}
+				catch (error)
+				{
+					removeFileSucceeded = false;
+					// Make an entry in the BBS log that deleting the file failed
+					var logMsg = "ddfilelister: " + error;
+					log(LOG_ERR, logMsg);
+					bbs.log_str(logMsg);
+				}
+				// If the remove failed with deleting the file, then try without deleting the file
+				if (!removeFileSucceeded)
+				{
+					removeFileSucceeded = filebase.remove(pFileList[fileIdx].name, false);
+					if (removeFileSucceeded)
+					{
+						var logMsg = "ddfilelister: Removed " + filenameFullPath + " from the "
+						           + "filebase but couldn't actually delete the file";
+						log(LOG_INFO, logMsg);
+						bbs.log_str(logMsg);
+					}
+				}
+				if (removeFileSucceeded)
+				{
+					if (gScriptMode == MODE_LIST_CURDIR)
+						numFilesRemaining = filebase.files;
+				}
 				filebase.close();
 			}
 			else
@@ -1293,7 +1341,7 @@ function confirmAndRemoveFilesFromFilebase(pFileList, pFileListMenu)
 			// Have the file list menu set up its description width, colors, and format
 			// string again in case it no longer needs to use its scrollbar
 			pFileListMenu.SetItemWidthsColorsAndFormatStr();
-			retObj.reDrawFileListMenu = true;
+			retObj.reDrawMainScreenContent = true;
 			// If all files were not in the same directory, then check to see if all
 			// remaining files are now.  If so, we'll need to update the header lines
 			// at the top of the file list.
@@ -1635,28 +1683,29 @@ function DDFileMenuBarItem(pItemText, pPos, pRetCode)
 //               fileTime: The timestamp of the file
 function getFileInfoFromFilebase(pDirCode, pFilename, pDetail)
 {
-	var retObj = {
-		fileMetadataObj: null,
-		fileTime: 0
-	};
-
 	if (typeof(pDirCode) !== "string" || pDirCode.length == 0 || typeof(pFilename) !== "string" || pFilename.length == 0)
-		return retObj;
+		return null;
 
+	var fileMetadataObj = null;
 	var filebase = new FileBase(pDirCode);
 	if (filebase.open())
 	{
 		// Just in case the file has the full path, get just the filename from it.
 		var filename = file_getname(pFilename);
 		var fileDetail = (typeof(pDetail) === "number" ? pDetail : FileBase.DETAIL.NORM);
-		retObj.fileMetadataObj = filebase.get(filename, fileDetail);
-		retObj.fileMetadataObj.dirCode = pDirCode;
-		//retObj.fileMetadataObj.size = filebase.get_size(filename);
-		retObj.fileTime = filebase.get_time(filename);
+		if (typeof(pDetail) === "number")
+			fileDetail = pDetail;
+		else
+			fileDetail = (extendedDescEnabled() ? FileBase.DETAIL.EXTENDED : FileBase.DETAIL.NORM);
+		fileMetadataObj = filebase.get(filename, fileDetail);
+		fileMetadataObj.dirCode = pDirCode;
+		//fileMetadataObj.size = filebase.get_size(filename);
+		if (!fileMetadataObj.hasOwnProperty("time"))
+			fileMetadataObj.time = filebase.get_time(filename);
 		filebase.close();
 	}
 
-	return retObj;
+	return fileMetadataObj;
 }
 
 // Moves a file from one filebase to another
@@ -1965,14 +2014,9 @@ function displayListHdrLine(pMoveToLocationFirst)
 		console.gotoxy(1, 3);
 	var filenameLen = gListIdxes.filenameEnd - gListIdxes.filenameStart;
 	var fileSizeLen = gListIdxes.fileSizeEnd - gListIdxes.fileSizeStart -1;
-	//var shortDescLen = gListIdxes.descriptionEnd - gListIdxes.descriptionStart + 1;
-	// shortDescLen here should always be the same (for the last blocks to always be in the same
-	// position), whereas descriptionEnd might change based on whether the menu is using its scrollbar
-	var shortDescLen = 60;
-	if (console.screen_columns > 80)
-		shortDescLen = console.screen_columns - 30;
+	var descLen = gListIdxes.descriptionEnd - gListIdxes.descriptionStart + 1;
 	var formatStr = "\1n\1w\1h%-" + filenameLen + "s %" + fileSizeLen + "s %-"
-	              + +(shortDescLen-7) + "s\1n\1w%5s\1n";
+	              + +(descLen-7) + "s\1n\1w%5s\1n";
 	var listHdrEndText = THIN_RECTANGLE_RIGHT + BLOCK4 + BLOCK3 + BLOCK2 + BLOCK1;
 	printf(formatStr, "Filename", "Size", "Description", listHdrEndText);
 }
@@ -1989,13 +2033,23 @@ function createFileListMenu(pQuitKeys)
 	// Create the menu object.  Place it below the header lines (which should have been written
 	// before this), and also leave 1 row at the bottom for the prompt line
 	var startRow = gNumHeaderLinesDisplayed > 0 ? gNumHeaderLinesDisplayed + 1 : 1;
-	var fileListMenu = new DDLightbarMenu(1, startRow, console.screen_columns - 1, console.screen_rows - (startRow-1) - 1);
+	// If we'll be displaying short (one-line) file descriptions, then use the whole width
+	// of the terminal (minus 1) for the menu width.  But if the user has extended (multi-line)
+	// file descriptions enabled, then set the menu width only up through the file size, since
+	// the extended file description will be displayed to the right of the menu.
+	var menuWidth = console.screen_columns - 1;
+	if (extendedDescEnabled())
+		menuWidth = gListIdxes.fileSizeEnd + 1;
+	var menuHeight = console.screen_rows - (startRow-1) - 1;
+	var fileListMenu = new DDLightbarMenu(1, startRow, menuWidth, menuHeight);
 	fileListMenu.scrollbarEnabled = true;
 	fileListMenu.borderEnabled = false;
 	fileListMenu.multiSelect = true;
 	fileListMenu.ampersandHotkeysInItems = false;
 	fileListMenu.wrapNavigation = false;
 
+	fileListMenu.extdDescEnabled = extendedDescEnabled();
+
 	// Add additional keypresses for quitting the menu's input loop so we can
 	// respond to these keys.
 	if (typeof(pQuitKeys) === "string")
@@ -2031,8 +2085,14 @@ function createFileListMenu(pQuitKeys)
 		this.filenameLen = gListIdxes.filenameEnd - gListIdxes.filenameStart;
 		this.fileSizeLen = gListIdxes.fileSizeEnd - gListIdxes.fileSizeStart -1;
 		this.shortDescLen = gListIdxes.descriptionEnd - gListIdxes.descriptionStart + 1;
-		this.fileFormatStr = "%-" + this.filenameLen + "s %" + this.fileSizeLen
-		                   + "s %-" + this.shortDescLen + "s";
+		// If extended descriptions are enabled, then we won't be writing a description here
+		if (this.extdDescEnabled)
+			this.fileFormatStr = "%-" + this.filenameLen + "s %" + this.fileSizeLen + "s";
+		else
+		{
+			this.fileFormatStr = "%-" + this.filenameLen + "s %" + this.fileSizeLen
+			                   + "s %-" + this.shortDescLen + "s";
+		}
 		return widthChanged;
 	};
 	// Set up the menu's description width, colors, and format string
@@ -2063,6 +2123,12 @@ function createFileListMenu(pQuitKeys)
 	fileListMenu.numSelectedItemIndexes = function() {
 		return Object.keys(this.selectedItemIndexes).length;
 	};
+
+	// OnItemNav function for when the user navigates to a new item
+	fileListMenu.OnItemNav = function(pOldItemIdx, pNewItemIdx) {
+		displayFileExtDescOnMainScreen(pNewItemIdx);
+	}
+
 	return fileListMenu;
 }
 
@@ -2493,7 +2559,8 @@ function eraseMsgBoxScreenArea()
 	// Refresh the list header line and have the file list menu refresh itself over
 	// the error message window
 	displayListHdrLine(true);
-	gFileListMenu.DrawPartialAbs(gErrorMsgBoxULX, gErrorMsgBoxULY+1, gErrorMsgBoxWidth, gErrorMsgBoxHeight-2);
+	// This used to call gFileListMenu.DrawPartialAbs
+	refreshScreenMainContent(gErrorMsgBoxULX, gErrorMsgBoxULY+1, gErrorMsgBoxWidth, gErrorMsgBoxHeight-2);
 }
 
 // Draws a border
@@ -2660,7 +2727,8 @@ function confirmFileActionWithUser(pFilenames, pActionName, pDefaultYes)
 			actionConfirmed = console.yesno(pActionName + " " + shortFilename);
 		else
 			actionConfirmed = !console.noyes(pActionName + " " + shortFilename);
-		gFileListMenu.DrawPartialAbs(1, console.screen_rows-2, console.screen_columns, 2, {});
+		// Refresh the main screen content, to erase the confirmation prompt
+		refreshScreenMainContent(1, console.screen_rows-2, console.screen_columns, 2, true);
 	}
 	else
 	{
@@ -2668,7 +2736,8 @@ function confirmFileActionWithUser(pFilenames, pActionName, pDefaultYes)
 		// user to delete the files
 		var frameUpperLeftX = gFileListMenu.pos.x + 2;
 		var frameUpperLeftY = gFileListMenu.pos.y + 2;
-		var frameWidth = gFileListMenu.size.width - 4;
+		//var frameWidth = gFileListMenu.size.width - 4;
+		var frameWidth = console.screen_columns - 4;
 		var frameHeight = 10;
 		var frameTitle = pActionName + " files? (Y/N)";
 		var additionalQuitKeys = "yYnN";
@@ -2681,7 +2750,8 @@ function confirmFileActionWithUser(pFilenames, pActionName, pDefaultYes)
 		                                                       frameTitle, gColors.confirmFileActionWindowWindowTitle,
 		                                                       fileListStr, additionalQuitKeys);
 		actionConfirmed = (lastUserInput.toUpperCase() == "Y");
-		gFileListMenu.DrawPartialAbs(frameUpperLeftX, frameUpperLeftY, frameWidth, frameHeight, {});
+		// Refresh the main screen content, to erase the confirmation window
+		refreshScreenMainContent(frameUpperLeftX, frameUpperLeftY, frameWidth, frameHeight, true);
 	}
 
 	return actionConfirmed;
@@ -3061,17 +3131,20 @@ function populateFileList(pSearchMode)
 				return retObj;
 			}
 
-			// To check a user's file basic/extended detail information setting:
-			// if ((user.settings & USER_EXTDESC) == USER_EXTDESC)
-
-			// Get a list of file data with normal detail (without extended info).  When the user
-			// selects a file to view extended info, we'll get metadata about the file with extended detail.
-			gFileList = filebase.get_list("*", FileBase.DETAIL.NORM, 0, true, gFileSortOrder); // FileBase.DETAIL.EXTENDED
+			// Get a list of file data
+			var fileDetail = (extendedDescEnabled() ? FileBase.DETAIL.EXTENDED : FileBase.DETAIL.NORM);
+			gFileList = filebase.get_list("*", fileDetail, 0, true, gFileSortOrder);
 			filebase.close();
 			// Add a dirCode property to the file metadata objects (for consistency,
 			// as file search results may contain files from multiple directories).
+			// Also, if the metadata objects have an extdesc, remove any trailing CRLF
+			// from the end.
 			for (var i = 0; i < gFileList.length; ++i)
+			{
 				gFileList[i].dirCode = bbs.curdir_code;
+				if (gFileList[i].hasOwnProperty("extdesc") && /\r\n$/.test(gFileList[i].extdesc))
+					gFileList[i].extdesc = gFileList[i].extdesc.substr(0, gFileList[i].extdesc.length-2);
+			}
 		}
 		else
 		{
@@ -3222,6 +3295,36 @@ function populateFileList(pSearchMode)
 		return retObj;
 	}
 
+	// Figure out the longest filename in the list.
+	var longestFilenameLen = 0;
+	for (var i = 0; i < gFileList.length; ++i)
+	{
+		if (gFileList[i].name.length > longestFilenameLen)
+			longestFilenameLen = gFileList[i].name.length;
+	}
+	var displayFilenameLen = gListIdxes.filenameEnd - gListIdxes.filenameStart + 1;
+	// If the user has extended descriptions enabled, then allow 47 characters for the
+	// description and adjust the filename length accordingly
+	gListIdxes.descriptionEnd = console.screen_columns - 1; // Leave 1 character remaining on the screen
+	if (extendedDescEnabled())
+	{
+		gListIdxes.descriptionStart = gListIdxes.descriptionEnd - 47 + 1;
+		gListIdxes.fileSizeEnd = gListIdxes.descriptionStart;
+		gListIdxes.fileSizeStart = gListIdxes.fileSizeEnd - 7;
+		gListIdxes.filenameEnd = gListIdxes.fileSizeStart;
+	}
+	// If not displaying extended descriptions, then if the longest filename
+	// is longer than the current display filename length and the user's
+	// terminal is at least 100 columns wide, then increase the filename length
+	// for the list by 20;
+	else if (longestFilenameLen > displayFilenameLen && console.screen_columns >= 100)
+	{
+		gListIdxes.filenameEnd += 20;
+		gListIdxes.fileSizeStart = gListIdxes.filenameEnd;
+		gListIdxes.fileSizeEnd = gListIdxes.fileSizeStart + 7;
+		gListIdxes.descriptionStart = gListIdxes.fileSizeEnd;
+	}
+
 	return retObj;
 }
 
@@ -3463,4 +3566,171 @@ function searchDirGroupOrAll(pSearchOption, pDirSearchFn)
 	}
 
 	return retObj;
+}
+
+// Returns whether the user has their extended file description setting enabled
+// and if it can be supported in the user's terminal mode.  Extended descriptions
+// will be displayed in the main screen if the user has that option enabled and
+// the user's terminal is at least 80 columns wide.
+function extendedDescEnabled()
+{
+	var userExtDescEnabled = ((user.settings & USER_EXTDESC) == USER_EXTDESC);
+	return userExtDescEnabled && console.screen_columns >= 80;
+}
+
+// Displays a file's extended description on the main screen, next to the
+// file list menu.  This is to be used when the user's extended file description
+// option is enabled (where the menu would take up about the left half of
+// the screen).
+//
+// Parameters:
+//  pFileIdx: The index of the file metadata object in gFileList to use
+//  pStartScreenRow: Optional - The screen row number to start printing, for partial
+//                   screen refreshing (can be in the middle of the extended description)
+//  pEndScreenRow: Optional - The screen row number to stop printing, for partial
+//                   screen refreshing (can be in the middle of the extended description)
+//  pMaxWidth: Optional - The maximum width to use for printing the description lines
+function displayFileExtDescOnMainScreen(pFileIdx, pStartScreenRow, pEndScreenRow, pMaxWidth)
+{
+	if (typeof(pFileIdx) !== "number")
+		return;
+	if (pFileIdx < 0 || pFileIdx >= gFileList.length)
+		return;
+
+	// Get the file description from its metadata object
+	var fileMetadata = gFileList[pFileIdx];
+	var fileDesc = "";
+	if (fileMetadata.hasOwnProperty("extdesc") && fileMetadata.extdesc.length > 0)
+		fileDesc = fileMetadata.extdesc;
+	else
+		fileDesc = fileMetadata.desc;
+
+	// This might be overkill, but just in case, convert any non-Synchronet
+	// attribute codes to Synchronet attribute codes in the description.
+	// This will help simplify getting substrings for formatting.  Then for
+	// efficiency, put the converted description back into the metadata
+	// object in the array so that it doesn't have to be converted again.
+	if (!fileMetadata.hasOwnProperty("attrsConverted"))
+	{
+		fileDesc = convertAttrsToSyncPerSysCfg(fileDesc);
+		fileMetadata.attrsConverted = true;
+		if (fileMetadata.hasOwnProperty("extdesc"))
+			fileMetadata.extdesc = fileDesc;
+		else
+			fileMetadata.desc = fileDesc;
+	}
+
+	// Calculate where to write the description on the screen
+	var startX = gFileListMenu.size.width + 1; // Assuming the file menu starts at the leftmost column
+	var maxDescLen = console.screen_columns - startX;
+	if (typeof(pMaxWidth) === "number" && pMaxWidth >= 0 && pMaxWidth < maxDescLen)
+		maxDescLen = pMaxWidth;
+	// Go to the location on the screen and write the file description
+	var formatStr = "%-" + maxDescLen + "s";
+	// firstScreenRow is the first row on the screen where the extended description
+	// should start at.  lastScreenRow is the last row (inclusive) to use for
+	// printing the extended description
+	var firstScreenRow = gNumHeaderLinesDisplayed + 1;
+	var lastScreenRow = console.screen_rows - 1; // This is inclusive
+	// screenRowForPrinting will be used for the actual screen row we're at while
+	// printing the extended description lines
+	var screenRowForPrinting = firstScreenRow;
+	// If pStartScreenRow or pEndScreenRow are specified, then use
+	// them to specify the start & end screen rows to actually print
+	if (typeof(pStartScreenRow) === "number" && pStartScreenRow >= firstScreenRow && pStartScreenRow <= lastScreenRow)
+		screenRowForPrinting = pStartScreenRow;
+	if (typeof(pEndScreenRow) === "number" && pEndScreenRow > firstScreenRow && pStartScreenRow <= lastScreenRow)
+		lastScreenRow = pEndScreenRow;
+	var fileDescArray = fileDesc.split("\r\n");
+	console.print("\1n");
+	// screenRowNum is to keep track of the row on the screen where the
+	// description line would be placed, in case the start row is after that
+	var screenRowNum = firstScreenRow;
+	for (var i = 0; i < fileDescArray.length; ++i)
+	{
+		if (screenRowForPrinting > screenRowNum++)
+			continue;
+		console.gotoxy(startX, screenRowForPrinting++);
+		// Note: substrWithAttrCodes() is defined in dd_lightbar_menu.js
+		// Normally it would be handy to use printf() to print the text line:
+		//printf(formatStr, substrWithAttrCodes(fileDescArray[i], 0, maxDescLen));
+		// However, printf() doesn't account for attribute codes and thus may not
+		// fill the rest of the width.  So, we do that manually.
+		var descLine = substrWithAttrCodes(fileDescArray[i], 0, maxDescLen);
+		console.print(descLine);
+		var remainingLen = maxDescLen - console.strlen(descLine);
+		if (remainingLen > 0)
+			printf("%" + remainingLen + "s", "");
+		// Stop printing the description lines when we reach the last line on
+		// the screen where we want to print.
+		if (screenRowForPrinting > lastScreenRow)
+			break;
+	}
+	// Clear the rest of the lines to the bottom of the list area
+	console.print("\1n");
+	while (screenRowForPrinting <= lastScreenRow)
+	{
+		console.gotoxy(startX, screenRowForPrinting++);
+			printf(formatStr, "");
+	}
+}
+
+// Refreshes (re-draws) the main content of the screen (file list menu,
+// and extended description area if enabled).  The coordinates are absolute
+// screen coordinates.
+//
+// Parameters:
+//  pUpperLeftX: The X coordinate of the upper-left corner of the area to re-draw
+//  pUpperLeftY: The Y coordinate of the upper-left corner of the area to re-draw
+//  pWidth: The width of the area to re-draw
+//  pHeight: The height of the area to re-draw
+//  pSelectedItemIdxes: Optional: An object with selected item indexes for the file menu.
+//                                If not passed, an empty object will be used.
+//                      This can also be a boolean, and if true, will refresh the
+//                      selected items on the file menu (with checkmarks) outside the
+//                      given top & bottom screen rows.
+function refreshScreenMainContent(pUpperLeftX, pUpperLeftY, pWidth, pHeight, pSelectedItemIdxes)
+{
+	var selectedItemIdxesIsValid = (typeof(pSelectedItemIdxes) === "object");
+	var selectedItemIdxes = (selectedItemIdxesIsValid ? pSelectedItemIdxes : {});
+	gFileListMenu.DrawPartialAbs(pUpperLeftX, pUpperLeftY, pWidth, pHeight, selectedItemIdxes);
+	// If pSelectedItemIdxes is a bool instead of an object and is true,
+	// refresh the selected items (with checkmarks) outside the top & bottom
+	// lines on the file menu
+	if (!selectedItemIdxesIsValid && typeof(pSelectedItemIdxes) === "boolean" && pSelectedItemIdxes && gFileListMenu.numSelectedItemIndexes() > 0)
+	{
+		var bottomScreenRow = pUpperLeftY + pHeight - 1;
+		for (var idx in gFileListMenu.selectedItemIndexes)
+		{
+			var idxNum = +idx;
+			var itemScreenRow = gFileListMenu.ScreenRowForItem(idxNum);
+			if (itemScreenRow == -1)
+				continue;
+			if (itemScreenRow < pUpperLeftY || itemScreenRow > bottomScreenRow)
+			{
+				var isSelected = (idxNum == gFileListMenu.selectedItemIdx);
+				gFileListMenu.WriteItemAtItsLocation(idxNum, isSelected, false);
+			}
+		}
+	}
+	// If the user has extended descriptions enabled, then the file menu
+	// is only taking up about half the screen on the left, and we'll also
+	// have to refresh the description area.
+	if (extendedDescEnabled())
+	{
+		var fileMenuRightX = gFileListMenu.pos.x + gFileListMenu.size.width - 1;
+		var width = pWidth - (fileMenuRightX - pUpperLeftX + 1);
+		if (width > 0)
+		{
+			var firstRow = pUpperLeftY;
+			// The last row is inclusive.  It seems like there might be an off-by-1
+			// problem here?  I thought 1 would need to be subtracted from lastRow
+			var lastRow = pUpperLeftY + pHeight;
+			// We don't want to overwrite the last row on the screen, since that's
+			// used for the command bar
+			if (lastRow == console.screen_rows)
+				--lastRow;
+			displayFileExtDescOnMainScreen(gFileListMenu.selectedItemIdx, firstRow, lastRow, width);
+		}
+	}
 }
\ No newline at end of file
diff --git a/xtrn/ddfilelister/readme.txt b/xtrn/ddfilelister/readme.txt
index e7e3c047f405f912f4111db15a5660c06af6638f..37dfeb53389d539be11ef09160f1e698861d43b8 100644
--- a/xtrn/ddfilelister/readme.txt
+++ b/xtrn/ddfilelister/readme.txt
@@ -1,6 +1,6 @@
                         Digital Distortion File Lister
-                                 Version 2.04
-                           Release date: 2022-03-09
+                                 Version 2.05
+                           Release date: 2022-03-12
 
                                      by
 
@@ -50,6 +50,14 @@ action.  The file lister also uses message boxes to display information.
 If the user's terminal does not support ANSI, the file lister will run the
 stock Synchronet file lister interface instead.
 
+Digital Distortion File Lister makes use of the user's extended description
+setting.  If the user's extended description setting is enabled, the lister
+will use a split interface, with the lightbar file list on the left side and
+the extended file description for the highlighted file displayed on the right
+side.  If the user's extended file description setting is disabled, the
+lightbar file menu will use the entire width of the screen, with the short
+file descriptions being displayed in a single row with each file.
+
 When adding files to the user's batch download queue or (for the sysop)
 selecting files to move or delete, multi-select mode can be used, allowing
 the user to select multiple files using the spacebar.  If the spacebar is not
diff --git a/xtrn/ddfilelister/revision_history.txt b/xtrn/ddfilelister/revision_history.txt
index ab234fd942d8b9988e987c75d9f36e72af7e5d04..f8c50f2d32728bb0600acdd100497fc63cd17d4e 100644
--- a/xtrn/ddfilelister/revision_history.txt
+++ b/xtrn/ddfilelister/revision_history.txt
@@ -5,6 +5,14 @@ Revision History (change log)
 =============================
 Version  Date         Description
 -------  ----         -----------
+2.05     2022-03-12   Now makes use of the user's extended file description
+                      setting: If the user's extended file description setting
+                      is enabled, the lister will now show extended file
+                      descriptions on the main screen in a split format, with
+                      the lightbar file list on the left and the extended file
+                      description for the highlighted file on the right.  Also,
+                      made the file info window taller for terminals within 25
+                      lines high.
 2.04     2022-03-09   Bug fix: Now successfully formats filenames without
                       extensions when listing files.
 2.03     2022-02-27   For terminals over 80 rows tall, the file info window will