From fc37b5853df85de8fba7d2d64b79781d957f30d9 Mon Sep 17 00:00:00 2001
From: Eric Oulashin <eric.oulashin@gmail.com>
Date: Sat, 12 Mar 2022 11:57:26 -0800
Subject: [PATCH] ddfilelister: Now displays extended description in list view
 if the user has that setting enabled ddfilelister 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. This should resolve issue #363 . This update to ddfilelister also
 requires the included update to dd_lightbar_menu.js and the new attr_conv.js.

---
 exec/load/attr_conv.js                 | 1132 ++++++++++++++++++++++++
 exec/load/dd_lightbar_menu.js          |  135 ++-
 xtrn/ddfilelister/ddfilelister.js      |  664 +++++++++-----
 xtrn/ddfilelister/readme.txt           |   12 +-
 xtrn/ddfilelister/revision_history.txt |    8 +
 5 files changed, 1748 insertions(+), 203 deletions(-)
 create mode 100644 exec/load/attr_conv.js

diff --git a/exec/load/attr_conv.js b/exec/load/attr_conv.js
new file mode 100644
index 0000000000..e3630db5fb
--- /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 1c0e7e4d8d..2046978fce 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 a1a9e10140..c54a109bdd 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 e7e3c047f4..37dfeb5338 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 ab234fd942..f8c50f2d32 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
-- 
GitLab