diff --git a/exec/default.js b/exec/default.js
new file mode 100755
index 0000000000000000000000000000000000000000..1d95e8b60a8d518ec5019280292edee906cd62ef
--- /dev/null
+++ b/exec/default.js
@@ -0,0 +1,255 @@
+// Default/Classic Synchronet Command Shell
+// replaces default.src/bin
+
+// @format.tab-size 4
+
+"use strict";
+
+require("sbbsdefs.js", "K_UPPER");
+require("userdefs.js", "UFLAG_T");
+require("nodedefs.js", "NODE_MAIN");
+require("key_defs.js", "KEY_UP");
+require("text.js", "Pause");
+bbs.revert_text(Pause);
+load("termsetup.js");
+var shell = load({}, "shell_lib.js");
+
+const help_key = '?';
+// If user has unlimited time, display time-used rather than time-remaining
+const time_code = user.security.exemptions & UFLAG_T ? "@TUSED@" : "@TLEFT@";
+
+const main_menu = {
+	file: "main",
+	eval: 'bbs.main_cmds++',
+	node_action: NODE_MAIN,
+	prompt: "\x01-\x01c\xfe \x01b\x01hMain \x01n\x01c\xfe \x01h" + time_code +
+		" \x01n\x01c[\x01h@GN@\x01n\x01c] @GRP@\x01\\ [\x01h@SN@\x01n\x01c] @SUB@: \x01n",
+	num_input: shell.get_sub_num,
+	slash_num_input: shell.get_grp_num,
+	command: {
+	 'A': { eval: 'bbs.auto_msg()' },
+	'/A': { exec: 'avatar_chooser.js'
+			,ars: 'ANSI and not GUEST'
+			,err: '\r\nSorry, only regular users with ANSI terminals can do that.\r\n' },
+	 'B': { eval: 'bbs.scan_subs(SCAN_BACK)'
+			,msg: '\r\n\x01c\x01hBrowse/New Message Scan\r\n' },
+	 'C': { eval: 'bbs.chat_sec()' },
+	 'D': { eval: 'bbs.user_config(); exit()' },
+	 'E': { exec: 'email_sec.js' },
+	 'F': { eval: 'bbs.scan_subs(SCAN_FIND)'
+			,msg: '\r\n\x01c\x01hFind Text in Messages\r\n' },
+	'/F': { eval: 'bbs.scan_subs(SCAN_FIND, /* all */true)' },
+	 'G': { eval: 'bbs.text_sec()' },
+	 'I': { eval: 'shell.main_info()' },
+	 'J': { eval: 'shell.select_msg_area()' },
+	 'L': { eval: 'bbs.list_msgs()' },
+	'/L': { eval: 'bbs.list_nodes()' },
+	 'M': { eval: 'bbs.time_bank()' },
+	 'N': { eval: 'bbs.scan_subs(SCAN_NEW)'
+			,msg: '\r\n\x01c\x01hNew Message Scan\r\n' },
+	'/N': { eval: 'bbs.scan_subs(SCAN_NEW, /* all */true)' },
+	 'O': { eval: 'shell.logoff(/* fast: */false)' },
+	'/O': { eval: 'shell.logoff(/* fast: */true)' },
+	 'P': { eval: 'bbs.post_msg()' },
+	'/P': { exec: 'postpoll.js' },
+	 'Q': { eval: 'bbs.qwk_sec()' },
+	 'R': { eval: 'bbs.scan_msgs()' },
+	 'S': { eval: 'bbs.scan_subs(SCAN_TOYOU)'
+			,msg: '\r\n\x01c\x01hScan for Messages Posted to You\r\n' },
+	'/S': { eval: 'bbs.scan_subs(SCAN_TOYOU, /* all */true)' },
+	 'U': { eval: 'shell.list_users()' },
+	'/U': { eval: 'bbs.list_users(UL_ALL)' },
+	 'V': { exec: 'scanpolls.js' },
+	'/V': { exec: 'scanpolls.js', args: ['all'] },
+	 'W': { eval: 'bbs.whos_online()' },
+	 'X': { eval: 'bbs.xtrn_sec()' },
+	'/X': { eval: 'exit(0)' },
+	 'Z': { eval: 'bbs.scan_subs(SCAN_NEW | SCAN_CONT)'
+			,msg: '\r\n\x01c\x01hContinuous New Message Scan\r\n' },
+	'/Z': { eval: 'bbs.scan_subs(SCAN_NEW | SCAN_CONT, /* all */true)' },
+	 '*': { eval: 'shell.show_subs(bbs.curgrp)' },
+	'/*': { eval: 'shell.show_grps()' },
+	 '&': { exec: 'msgscancfg.js' },
+	 '!': { eval: 'bbs.menu("sysmain")'
+			,ars: 'SYSOP or EXEMPT Q or I or N' },
+	 '#': {  msg: '\r\n\x01c\x01hType the actual number, not the symbol.\r\n' },
+	'/#': {  msg: '\r\n\x01c\x01hType the actual number, not the symbol.\r\n' },
+	},
+	nav: {
+	'\r': { },
+	 'T': { eval: 'shell.enter_file_section(); menu = file_menu' },
+	 '>': { eval: 'shell.sub_up()' },
+	 '}': { eval: 'shell.sub_up()' },
+	 ')': { eval: 'shell.sub_up()' },
+	 '+': { eval: 'shell.sub_up()' },
+	 '=': { eval: 'shell.sub_up()' },
+	 '<': { eval: 'shell.sub_down()' },
+	 '{': { eval: 'shell.sub_down()' },
+	 '(': { eval: 'shell.sub_down()' },
+	 '-': { eval: 'shell.sub_down()' },
+	 ']': { eval: 'shell.grp_up()' },
+	 '[': { eval: 'shell.grp_down()' },
+	},
+};
+
+// Can't do these statically through initialization:
+main_menu.nav[KEY_UP] = { eval: 'shell.sub_up()' };
+main_menu.nav[KEY_DOWN] = { eval: 'shell.sub_down()' };
+main_menu.nav[KEY_RIGHT] = { eval: 'shell.grp_up()' };
+main_menu.nav[KEY_LEFT] = { eval: 'shell.grp_down()' };
+
+const file_menu = {
+	file: "transfer",
+	eval: 'bbs.file_cmds++',
+	node_action: NODE_XFER,
+	prompt: "\x01-\x01c\xfe \x01b\x01hFile \x01n\x01c\xfe \x01h" + time_code +
+		" \x01n\x01c(\x01h@LN@\x01n\x01c) @LIB@\x01\\ (\x01h@DN@\x01n\x01c) @DIR@: \x01n",
+	num_input: shell.get_dir_num,
+	slash_num_input: shell.get_lib_num,
+	command: {
+	 'B': { eval: 'bbs.batch_menu()' },
+	 'C': { eval: 'bbs.chat_sec()' },
+	 'D': { eval: 'shell.download_files()'
+			,msg: '\r\n\x01c\x01hDownload File(s)\r\n'
+			,ars: 'REST NOT D' },
+	'/D': { eval: 'shell.download_user_files()'
+			,msg: '\r\n\x01c\x01hDownload File(s) from User(s)\r\n'
+			,ars: 'REST NOT D' },
+	 'E': { eval: 'shell.view_file_info(FI_INFO)'
+			,msg: '\r\n\x01c\x01hList Extended File Information\r\n' },
+	 'F': { eval: 'bbs.scan_dirs(FL_FINDDESC);'
+			,msg: '\r\n\x01c\x01hFind Text in File Descriptions (no wildcards)\r\n' },
+	'/F': { eval: 'bbs.scan_dirs(FL_FINDDESC, /* all: */true);' },
+	 'I': { eval: 'shell.file_info()' },
+	 'J': { eval: 'shell.select_file_area()' },
+	 'L': { eval: 'shell.list_files()' },
+	'/L': { eval: 'bbs.list_nodes()' },
+	 'N': { eval: 'bbs.scan_dirs(FL_ULTIME)'
+			,msg: '\r\n\x01c\x01hNew File Scan\r\n' },
+	'/N': { eval: 'bbs.scan_dirs(FL_ULTIME, /* all */true)' },
+	 'O': { eval: 'shell.logoff(/* fast: */false)' },
+	'/O': { eval: 'shell.logoff(/* fast: */true)' },
+	 'R': { eval: 'shell.view_file_info(FI_REMOVE)'
+			,msg: '\r\n\x01c\x01hRemove/Edit File(s)\r\n' },
+	 'S': { eval: 'bbs.scan_dirs(FL_NO_HDR)'
+			,msg: '\r\n\x01c\x01hSearch for Filename(s)\r\n' },
+	'/S': { eval: 'bbs.scan_dirs(FL_NO_HDR, /* all */true) ' },
+	 'T': { eval: 'bbs.temp_xfer()' },
+	 'U': { eval: 'shell.upload_file()'
+			,msg: '\r\n\x01c\x01hUpload File\r\n' },
+	'/U': { eval: 'shell.upload_user_file()'
+			,msg: '\r\n\x01c\x01hUpload File to User\r\n' },
+	 'V': { eval: 'shell.view_files()'
+			,msg: '\r\n\x01c\x01hView File(s)\r\n' },
+	 'W': { eval: 'bbs.whos_online()' },
+	 'Z': { eval: 'shell.upload_sysop_file()'
+			,msg: '\r\n\x01c\x01hUpload File to Sysop\r\n' },
+	 '*': { eval: 'shell.show_dirs(bbs.curlib)' },
+	'/*': { eval: 'shell.show_libs()' },
+	 '&': { exec: 'filescancfg.js' },
+	 '!': { eval: 'bbs.menu("sysxfer")' },
+	 '#': {  msg: '\r\n\x01c\x01hType the actual number, not the symbol.\r\n' },
+	'/#': {  msg: '\r\n\x01c\x01hType the actual number, not the symbol.\r\n' },
+	},
+	nav: {
+	'\r': { },
+	 'Q': { eval: 'menu = main_menu' },
+	 '>': { eval: 'shell.dir_up()' },
+	 '}': { eval: 'shell.dir_up()' },
+	 ')': { eval: 'shell.dir_up()' },
+	 '+': { eval: 'shell.dir_up()' },
+	 '=': { eval: 'shell.dir_up()' },
+	 '<': { eval: 'shell.dir_down()' },
+	 '{': { eval: 'shell.dir_down()' },
+	 '(': { eval: 'shell.dir_down()' },
+	 '-': { eval: 'shell.dir_down()' },
+	 ']': { eval: 'shell.lib_up()' },
+	 '[': { eval: 'shell.lib_down()' },
+	},
+};
+
+// Can't do these statically through initialization:
+file_menu.nav[KEY_UP] = { eval: 'shell.dir_up()' };
+file_menu.nav[KEY_DOWN] = { eval: 'shell.dir_down()' };
+file_menu.nav[KEY_RIGHT] = { eval: 'shell.lib_up()' };
+file_menu.nav[KEY_LEFT] = { eval: 'shell.lib_down()' };
+
+var menu = main_menu;
+var last_str_cmd = "";
+
+// The menu-display/command-prompt loop
+while(bbs.online && !js.terminated) {
+	if(!(user.settings & USER_EXPERT)) {
+		console.clear();
+		bbs.menu(menu.file);
+	}
+	bbs.node_action = menu.node_action;
+	bbs.nodesync();
+	eval(menu.eval);
+	console.crlf();
+	console.aborted = false;
+	console.putmsg(menu.prompt, P_SAVEATR);
+	var cmd = console.getkey(K_UPPER);
+	log("cmd = " + ascii(cmd));
+	if(cmd > ' ')
+		console.print(cmd);
+	if(cmd == ';') {
+		cmd = console.getstr();
+		if(cmd == '!')
+			cmd = last_str_cmd;
+		js.exec("str_cmds.js", {}, cmd);
+		last_str_cmd = cmd;
+		console.line_counter = 0;
+		continue;
+	}
+	if(cmd == '/') {
+		cmd = console.getkey(K_UPPER);
+		console.print(cmd);
+		if(cmd >= '1' && cmd <= '9') {
+			menu.slash_num_input(cmd);
+			continue;
+		}
+		cmd = '/' + cmd;
+	}
+	if(cmd >= '1' && cmd <= '9') {
+		menu.num_input(cmd);
+		continue;
+	}
+	if(cmd > ' ') {
+		bbs.log_key(cmd, /* comma: */true);
+	}
+	if(menu.nav[cmd]) {
+		if(menu.nav[cmd].eval)
+			eval(menu.nav[cmd].eval);
+		continue;
+	}
+	console.crlf();
+	console.line_counter = 0;
+	if(cmd == help_key) {
+		if(user.settings & USER_EXPERT)
+			bbs.menu(menu.file);
+		continue;
+	}
+	var menu_cmd = menu.command[cmd];
+	if(!menu_cmd) {
+		console.print("\r\n\x01c\x01hUnrecognized command.");
+		if(user.settings & USER_EXPERT)
+			console.print(" Hit '\x01i" + help_key + "\x01n\x01c\x01h' for a menu.");
+		console.crlf();
+		continue;
+	}
+	if(!bbs.compare_ars(menu_cmd.ars))
+		console.print(menu_cmd.err);
+	else {
+		if(menu_cmd.msg)
+			console.print(menu_cmd.msg);
+		if(menu_cmd.eval)
+			eval(menu_cmd.eval);
+		if(menu_cmd.exec) {
+			if(menu_cmd.args)
+				js.exec.apply(null, [menu_cmd.exec, {}].concat(menu_cmd.args));
+			else
+				js.exec(menu_cmd.exec, {});
+		}
+	}
+}
diff --git a/exec/load/shell_lib.js b/exec/load/shell_lib.js
new file mode 100755
index 0000000000000000000000000000000000000000..00bdc15356ed403ff09baf75205e3d4b90e24b76
--- /dev/null
+++ b/exec/load/shell_lib.js
@@ -0,0 +1,549 @@
+// Library for writing command shells
+// portions derived from classic_shell.js, default.src, and exec*.cpp
+
+// @format.tab-size 4
+
+"use strict";
+
+require("text.js", "DownloadBatchQ");
+require("sbbsdefs.js", "USER_EXPERT");
+
+// Build list of current subs/dirs in each group/library
+// This hack is required because the 'bbs' object doesn't expose the current
+// sub/dir for any group/library except the current
+var curgrp = bbs.curgrp;
+var curlib = bbs.curlib;
+var cursub = [];
+var curdir = [];
+var usrsubs = [];
+var usrdirs = [];
+var usrgrps = msg_area.grp_list.length;
+var usrlibs = file_area.lib_list.length;
+for(var i = 0; i < usrgrps; ++i) {
+	bbs.curgrp = i;
+	cursub[i] = bbs.cursub;
+	usrsubs[i] = msg_area.grp_list[i].sub_list.length;
+}
+for(var i = 0; i < usrlibs; ++i) {
+	bbs.curlib = i;
+	curdir[i] = bbs.curdir;
+	usrdirs[i] = file_area.lib_list[i].dir_list.length;
+}
+bbs.curgrp = curgrp;
+bbs.curlib = curlib;
+
+function get_num(str, max)
+{
+	var num = Number(str);
+	while(bbs.online && !js.terminated) {
+		if(num * 10 > max)
+			break;
+		var ch = console.getkey(K_UPPER);
+		if(ch < '0' || ch > '9') {
+			if(ch > ' ')
+				console.ungetstr(ch);
+			break;
+		}
+		console.print(ch);
+		num *= 10;
+		num += Number(ch);
+		if(num > max)
+			return 0;
+	}
+	return num;
+}
+
+function get_grp_num(str)
+{
+	var num = get_num(str, usrgrps);
+	if(num > 0)
+		bbs.curgrp = num -1;
+}
+
+function get_sub_num(str)
+{
+	var num = get_num(str, usrsubs[bbs.curgrp]);
+	if(num > 0)
+		bbs.cursub = num - 1;
+}
+
+function get_lib_num(str)
+{
+	var num = get_num(str, usrlibs);
+	if(num > 0)
+		bbs.curlib = num -1;
+}
+
+function get_dir_num(str)
+{
+	var num = get_num(str, usrdirs[bbs.curlib]);
+	if(num > 0)
+		bbs.curdir = num - 1;
+}
+
+// List Message Groups
+function show_grps()
+{
+	if(msg_area.grp_list.length < 1)
+		return;
+	if(bbs.menu("grps", P_NOERROR))
+		return;
+	console.print(bbs.text(GrpLstHdr));
+	for(var i=0; i < msg_area.grp_list.length && !console.aborted; i++) {
+		if(i == bbs.curgrp)
+			console.print('*');
+		else console.print(' ');
+		if(i<9) console.print(' ');
+		console.add_hotspot(i+1);
+		console.print(format(bbs.text(GrpLstFmt), i+1
+			,msg_area.grp_list[i].description, "", msg_area.grp_list[i].sub_list.length));
+	}
+}
+
+function show_subs(grp)
+{
+	if(msg_area.grp_list.length < 1)
+		return;
+	if(bbs.menu("subs" + msg_area.grp_list[grp].number, P_NOERROR))
+		return;
+	console.crlf();
+	console.print(format(bbs.text(SubLstHdr), msg_area.grp_list[grp].description));
+	for(var i=0; i < usrsubs[grp] && !console.aborted; ++i) {
+		if(i==cursub[grp]) console.print('*');
+		else console.print(' ');
+		var str = format(bbs.text(SubLstFmt),i+1
+			,msg_area.grp_list[grp].sub_list[i].description, ""
+			,msg_area.grp_list[grp].sub_list[i].posts);
+		if(i<9) console.print(' ');
+		if(i<99) console.print(' ');
+		console.add_hotspot(i+1);
+		console.print(str);
+	}
+}
+
+function select_msg_area()
+{
+	if(usrgrps < 1)
+		return;
+	while(bbs.online) {
+		var j=0;
+		if(usrgrps > 1) {
+			show_grps();
+			console.mnemonics(format(bbs.text(JoinWhichGrp), bbs.curgrp + 1));
+			j=console.getnum(usrgrps);
+			console.clear_hotspots();
+			if(j==-1)
+				return;
+			if(!j)
+				j=bbs.curgrp;
+			else
+				j--;
+		}
+		show_subs(j);
+		console.mnemonics(format(bbs.text(JoinWhichSub), cursub[j]+1));
+		i=console.getnum(usrsubs[j]);
+		console.clear_hotspots();
+		if(i==-1) {
+			if(usrgrps==1)
+				return;
+			continue;
+		}
+		if(!i)
+			i=cursub[j];
+		else
+			i--;
+		bbs.curgrp=j;
+		bbs.cursub=i;
+		return;
+	}
+	return;
+}
+
+// List File Libraries
+function show_libs()
+{
+	if(file_area.lib_list.length < 1)
+		return;
+	if(bbs.menu("libs", P_NOERROR))
+		return;
+	console.print(bbs.text(LibLstHdr));
+	for(var i=0; i < file_area.lib_list.length && !console.aborted; i++) {
+		if(i == bbs.curlib)
+			console.print('*');
+		else console.print(' ');
+		if(i<9) console.print(' ');
+		console.add_hotspot(i+1);
+		console.print(format(bbs.text(LibLstFmt), i+1
+			,file_area.lib_list[i].description, "", file_area.lib_list[i].dir_list.length));
+	}
+}
+
+function show_dirs(lib)
+{
+	if(file_area.lib_list.length < 1)
+		return;
+	if(bbs.menu("dirs" + file_area.lib_list[lib].number, P_NOERROR))
+		return;
+	console.crlf();
+	console.print(format(bbs.text(DirLstHdr), file_area.lib_list[lib].description));
+	for(var i=0; i < usrdirs[lib] && !console.aborted; ++i) {
+		if(i==curdir[lib]) console.print('*');
+		else console.print(' ');
+		var str = format(bbs.text(DirLstFmt),i+1
+			,file_area.lib_list[lib].dir_list[i].description, ""
+			,file_area.lib_list[lib].dir_list[i].files);
+		if(i<9) console.print(' ');
+		if(i<99) console.print(' ');
+		console.add_hotspot(i+1);
+		console.print(str);
+	}
+}
+
+function select_file_area()
+{
+	var usrlibs = file_area.lib_list.length;
+	if(usrlibs < 1)
+		return;
+	while(bbs.online) {
+		var j=0;
+		if(usrlibs > 1) {
+			show_libs();
+			console.mnemonics(format(bbs.text(JoinWhichGrp), bbs.curlib + 1));
+			j=console.getnum(usrlibs);
+			console.clear_hotspots();
+			if(j==-1)
+				return;
+			if(!j)
+				j=bbs.curlib;
+			else
+				j--;
+		}
+		show_dirs(j);
+		console.mnemonics(format(bbs.text(JoinWhichDir), curdir[j]+1));
+		i=console.getnum(usrdirs[j]);
+		console.clear_hotspots();
+		if(i==-1) {
+			if(usrlibs==1)
+				return;
+			continue;
+		}
+		if(!i)
+			i=curdir[j];
+		else
+			i--;
+		bbs.curlib=j;
+		bbs.curdir=i;
+		return;
+	}
+	return;
+}
+
+function main_info()
+{
+	while(bbs.online && !js.terminate) {
+		if(!(user.settings & USER_EXPERT))
+			bbs.menu("maininfo");
+		bbs.nodesync();
+		console.print("\r\n\x01y\x01hInfo: \x01n");
+		var key = console.getkeys("?QISVY\r");
+		bbs.log_key(key);
+		switch(key) {
+		case '?':
+			if(user.settings & USER_EXPERT)
+				bbs.menu("maininfo");
+			break;
+		case 'I':
+			bbs.sys_info();
+			break;
+		case 'S':
+			bbs.sub_info();
+			break;
+		case 'Y':
+			bbs.user_info();
+			break;
+		case 'V':
+			bbs.ver();
+			break;
+		default:
+			return;
+		}
+	}
+}
+
+// Prompts for new-file-scan the first time the user enters the file section
+function enter_file_section()
+{
+	if(bbs.file_cmds > 0)
+		return;
+	if(!(user.settings & USER_ASK_NSCAN))
+		return;
+	console.crlf(2);
+	if(console.yesno("Search all libraries for new files"))
+		bbs.scan_dirs(FL_ULTIME, /* all */true);
+}
+
+function file_info()
+{
+	var key;
+
+	while(1) {
+		if(!(user.settings & USER_EXPERT))
+			bbs.menu("xferinfo");
+
+		// async
+		console.print("\r\n\x01y\x01hInfo: \x01n");
+		key=console.getkeys("?TYDUQ\r");
+		bbs.log_key(key);
+
+		switch(key) {
+			case '?':
+				if(user.settings & USER_EXPERT)
+					bbs.menu("xferinfo");
+				break;
+
+			case 'T':
+				bbs.xfer_policy();
+				break;
+
+			case 'Y':
+				bbs.user_info();
+				break;
+
+			case 'D':
+				bbs.dir_info();
+				break;
+
+			case 'U':
+				bbs.list_users(UL_DIR);
+				break;
+
+			case 'Q':
+			default:
+				return;
+		}
+	}
+}
+
+function list_users()
+{
+	console.print("\r\n\x01c\x01hList Users\r\n");
+	console.mnemonics("\r\n~Logons Today, ~Yesterday, ~Sub-board, or ~All: ");
+	switch(console.getkeys("LSAY\r")) {
+	case 'L':
+		bbs.list_logons();
+		break;
+	case 'Y':
+		bbs.exec("?logonlist -y");
+		break;
+	case 'S':
+		bbs.list_users(UL_SUB);
+		break;
+	case 'A':
+		bbs.list_users(UL_ALL);
+		break;
+	}
+}
+
+function list_files()
+{
+	var result = bbs.list_files();
+	if(result < 0)
+		return;
+	if(result == 0)
+		console.print(bbs.text(EmptyDir));
+	else
+		console.print(format(bbs.text(NFilesListed), result));
+}
+
+function view_file_info(mode)
+{
+	var str=bbs.get_filespec();
+	if(!str)
+		return;
+	if(!bbs.list_file_info(file_area.lib_list[bbs.curlib].dir_list[bbs.curdir].number, str, mode)) {
+		var s=0;
+		console.print(bbs.text(SearchingAllDirs));
+		for(var i=0; i<file_area.lib_list[bbs.curlib].dir_list.length; i++) {
+			if(i!=bbs.curdir &&
+					(s=bbs.list_file_info(file_area.lib_list[bbs.curlib].dir_list[i].number, str, mode))!=0) {
+				if(s==-1 || str.indexOf('?')!=-1 || str.indexOf('*')!=-1) {
+					return;
+				}
+			}
+		}
+		console.print(bbs.text(SearchingAllLibs));
+		for(var i=0; i<file_area.lib_list.length; i++) {
+			if(i==bbs.curlib)
+				continue;
+			for(j=0; j<file_area.lib_list[i].dir_list.length; j++) {
+				if((s=bbs.list_file_info(file_area.lib_list[i].dir_list[j].number, str, mode))!=0) {
+					if(s==-1 || str.indexOf('?')!=-1 || str.indexOf('*')!=-1) {
+						return;
+					}
+				}
+			}
+		}
+	}
+}
+
+function view_files()
+{
+	var str=bbs.get_filespec();
+	if(!str)
+		return;
+	if(!bbs.list_files(file_area.lib_list[bbs.curlib].dir_list[bbs.curdir].number, str, FL_VIEW)) {
+		console.print(bbs.text(SearchingAllDirs));
+		for(var i=0; i<file_area.lib_list[bbs.curlib].dir_list.length; i++) {
+			if(i==bbs.curdir)
+				continue;
+			if(bbs.list_files(file_area.lib_list[bbs.curlib].dir_list[i].number, str, FL_VIEW))
+				break;
+		}
+		if(i<file_area.lib_list[bbs.curlib].dir_list.length)
+			return;
+		console.print(bbs.text(SearchingAllLibs));
+		for(var i=0; i<file_area.lib_list.length; i++) {
+			if(i==bbs.curlib)
+				continue;
+			for(j=0; j<file_area.lib_list[i].dir_list.length; j++) {
+				if(bbs.list_files(file_area.lib_list[i].dir_list[j].number, str, FL_VIEW))
+					return;
+			}
+		}
+	}
+}
+
+function sub_up()
+{
+	if(bbs.cursub == msg_area.grp_list[bbs.curgrp].sub_list.length - 1)
+		bbs.cursub = 0;
+	else
+		bbs.cursub++;
+	cursub[bbs.curgrp] = bbs.cursub;
+}
+
+function sub_down()
+{
+	if(bbs.cursub == 0)
+		bbs.cursub = msg_area.grp_list[bbs.curgrp].sub_list.length - 1;
+	else
+		bbs.cursub--;
+	cursub[bbs.curgrp] = bbs.cursub;
+}
+
+function grp_up()
+{
+	if(bbs.curgrp == msg_area.grp_list.length - 1)
+		bbs.curgrp = 0;
+	else
+		bbs.curgrp++;
+}
+
+function grp_down()
+{
+	if(bbs.curgrp == 0)
+		bbs.curgrp = msg_area.grp_list.length - 1;
+	else
+		bbs.curgrp--;
+}
+
+function dir_up()
+{
+	if(bbs.curdir == file_area.lib_list[bbs.curlib].dir_list.length - 1)
+		bbs.curdir = 0;
+	else
+		bbs.curdir++;
+	curdir[bbs.curdir] = bbs.curdir;
+}
+
+function dir_down()
+{
+	if(bbs.curdir == 0)
+		bbs.curdir = file_area.lib_list[bbs.curlib].dir_list.length - 1;
+	else
+		bbs.curdir--;
+	curdir[bbs.curdir] = bbs.curdir;
+}
+
+function lib_up()
+{
+	if(bbs.curlib == file_area.lib_list.length - 1)
+		bbs.curlib = 0;
+	else
+		bbs.curlib++;
+}
+
+function lib_down()
+{
+	if(bbs.curlib == 0)
+		bbs.curlib = file_area.lib_list.length - 1;
+	else
+		bbs.curlib--;
+}
+
+function logoff(fast)
+{
+	if(bbs.batch_dnload_total && console.yesno(bbs.text(DownloadBatchQ)))
+		bbs.batch_download();
+	else {
+		if(fast)
+			bbs.hangup();
+		else
+			bbs.logoff(/* prompt: */true);
+	}
+}
+
+function upload_file()
+{
+	bbs.menu("upload");
+	var i=0xffff;	/* INVALID_DIR */
+	if(usrlibs) {
+		i = file_area.lib_list[bbs.curlib].dir_list[bbs.curdir].number;
+		if(file_area.upload_dir != undefined
+			&& !file_area.lib_list[bbs.curlib].dir_list[bbs.curdir].can_upload)
+			i = file_area.upload_dir.number;
+	}
+	else {
+		if(file_area.upload_dir != undefined)
+			i = file_area.upload_dir.number;
+	}
+	bbs.upload_file(i);
+}
+
+function upload_user_file()
+{
+	if(file_area.user_dir == undefined)
+		console.print(bbs.text(NoUserDir));
+	else
+		bbs.upload_file(file_area.user_dir.number);
+}
+
+function upload_sysop_file()
+{
+	if(file_area.sysop_dir == undefined)
+		console.print(bbs.text(NoSysopDir));
+	else
+		bbs.upload_file(file_area.sysop_dir.number);
+}
+
+function download_files()
+{
+	if(bbs.batch_dnload_total && console.yesno(bbs.text(DownloadBatchQ))) {
+		bbs.batch_download();
+		return;
+	}
+
+	view_file_info(FI_DOWNLOAD);
+}
+
+function download_user_files()
+{
+	if(file_area.user_dir == undefined)
+		console.print(bbs.text(NoUserDir));
+	else {
+		if(!bbs.list_file_info(file_area.user_dir, FI_USERXFER))
+			console.print(bbs.text(NoFilesForYou));
+	}
+}
+
+this;