From e83ce0cdca65bf3205834c1fdcd6d58ac2a53355 Mon Sep 17 00:00:00 2001
From: "Rob Swindell (on Debian Linux)" <rob@synchro.net>
Date: Mon, 13 Mar 2023 21:28:44 -0700
Subject: [PATCH] Synchronet classic shell ported to JS with helper library:
 shell_lib.js

This mimics default.src very closely, even the curious clear-screens before
the menu displays only when in non-expert mode (?).

I'll likely be using this to recreate some other command shells.

There's a couple of RIP-specific calls to getlines in default.src that I did
not port over (yet).

This commit fixes issue #526 for Nelgin (and any other JavaScript devs).

Note: this file supercedes default.bin, so beware if you have customized
default.src (and built your own custom default.bin), you'll want to move
those to your mods directory to continue to use them.

Another nail in Baja's coffin.
---
 exec/default.js        | 255 +++++++++++++++++++
 exec/load/shell_lib.js | 549 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 804 insertions(+)
 create mode 100755 exec/default.js
 create mode 100755 exec/load/shell_lib.js

diff --git a/exec/default.js b/exec/default.js
new file mode 100755
index 0000000000..1d95e8b60a
--- /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 0000000000..00bdc15356
--- /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;
-- 
GitLab