Sync with master
This commit is contained in:
@@ -11,12 +11,14 @@ const {
|
||||
trackDoorRunBegin,
|
||||
trackDoorRunEnd
|
||||
} = require('./door_util.js');
|
||||
const Log = require('./logger').log;
|
||||
|
||||
// deps
|
||||
const async = require('async');
|
||||
const assert = require('assert');
|
||||
const _ = require('lodash');
|
||||
const paths = require('path');
|
||||
const fs = require('graceful-fs');
|
||||
|
||||
const activeDoorNodeInstances = {};
|
||||
|
||||
@@ -70,20 +72,12 @@ exports.getModule = class AbracadabraModule extends MenuModule {
|
||||
// :TODO: MenuModule.validateConfig(cb) -- validate config section gracefully instead of asserts! -- { key : type, key2 : type2, ... }
|
||||
// .. and/or EnigAssert
|
||||
assert(_.isString(this.config.name, 'Config \'name\' is required'));
|
||||
assert(_.isString(this.config.dropFileType, 'Config \'dropFileType\' is required'));
|
||||
assert(_.isString(this.config.cmd, 'Config \'cmd\' is required'));
|
||||
|
||||
this.config.nodeMax = this.config.nodeMax || 0;
|
||||
this.config.args = this.config.args || [];
|
||||
}
|
||||
|
||||
/*
|
||||
:TODO:
|
||||
* disconnecting while door is open leaves dosemu
|
||||
* http://bbslink.net/sysop.php support
|
||||
* Font support ala all other menus... or does this just work?
|
||||
*/
|
||||
|
||||
incrementActiveDoorNodeInstances() {
|
||||
if(activeDoorNodeInstances[this.config.name]) {
|
||||
activeDoorNodeInstances[this.config.name] += 1;
|
||||
@@ -141,11 +135,15 @@ exports.getModule = class AbracadabraModule extends MenuModule {
|
||||
return self.doorInstance.prepare(self.config.io || 'stdio', callback);
|
||||
},
|
||||
function generateDropfile(callback) {
|
||||
const dropFileOpts = {
|
||||
fileType : self.config.dropFileType,
|
||||
};
|
||||
if (!self.config.dropFileType || self.config.dropFileType.toLowerCase() === 'none') {
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
self.dropFile = new DropFile(
|
||||
self.client,
|
||||
{ fileType : self.config.dropFileType }
|
||||
);
|
||||
|
||||
self.dropFile = new DropFile(self.client, dropFileOpts);
|
||||
return self.dropFile.createFile(callback);
|
||||
}
|
||||
],
|
||||
@@ -170,17 +168,30 @@ exports.getModule = class AbracadabraModule extends MenuModule {
|
||||
args : this.config.args,
|
||||
io : this.config.io || 'stdio',
|
||||
encoding : this.config.encoding || 'cp437',
|
||||
dropFile : this.dropFile.fileName,
|
||||
dropFilePath : this.dropFile.fullPath,
|
||||
node : this.client.node,
|
||||
env : this.config.env,
|
||||
};
|
||||
|
||||
if (this.dropFile) {
|
||||
exeInfo.dropFile = this.dropFile.fileName;
|
||||
exeInfo.dropFilePath = this.dropFile.fullPath;
|
||||
}
|
||||
|
||||
const doorTracking = trackDoorRunBegin(this.client, this.config.name);
|
||||
|
||||
this.doorInstance.run(exeInfo, () => {
|
||||
trackDoorRunEnd(doorTracking);
|
||||
this.decrementActiveDoorNodeInstances();
|
||||
|
||||
// Clean up dropfile, if any
|
||||
if (exeInfo.dropFilePath) {
|
||||
fs.unlink(exeInfo.dropFilePath, err => {
|
||||
if (err) {
|
||||
Log.warn({ error : err, path : exeInfo.dropFilePath }, 'Failed to remove drop file.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// client may have disconnected while process was active -
|
||||
// we're done here if so.
|
||||
if(!this.client.term.output) {
|
||||
@@ -199,7 +210,7 @@ exports.getModule = class AbracadabraModule extends MenuModule {
|
||||
'\r\n\r\n'
|
||||
);
|
||||
|
||||
this.prevMenu();
|
||||
this.autoNextMenu();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -21,8 +21,6 @@ function ANSIEscapeParser(options) {
|
||||
events.EventEmitter.call(this);
|
||||
|
||||
this.column = 1;
|
||||
this.row = 1;
|
||||
this.scrollBack = 0;
|
||||
this.graphicRendition = {};
|
||||
|
||||
this.parseState = {
|
||||
@@ -36,11 +34,15 @@ function ANSIEscapeParser(options) {
|
||||
trailingLF : 'default', // default|omit|no|yes, ...
|
||||
});
|
||||
|
||||
|
||||
this.mciReplaceChar = miscUtil.valueWithDefault(options.mciReplaceChar, '');
|
||||
this.termHeight = miscUtil.valueWithDefault(options.termHeight, 25);
|
||||
this.termWidth = miscUtil.valueWithDefault(options.termWidth, 80);
|
||||
this.trailingLF = miscUtil.valueWithDefault(options.trailingLF, 'default');
|
||||
|
||||
|
||||
this.row = Math.min(options?.startRow ?? 1, this.termHeight);
|
||||
|
||||
self.moveCursor = function(cols, rows) {
|
||||
self.column += cols;
|
||||
self.row += rows;
|
||||
@@ -69,14 +71,11 @@ function ANSIEscapeParser(options) {
|
||||
};
|
||||
|
||||
self.clearScreen = function() {
|
||||
// :TODO: should be doing something with row/column?
|
||||
self.column = 1;
|
||||
self.row = 1;
|
||||
self.emit('clear screen');
|
||||
};
|
||||
|
||||
/*
|
||||
self.rowUpdated = function() {
|
||||
self.emit('row update', self.row + self.scrollBack);
|
||||
};*/
|
||||
|
||||
self.positionUpdated = function() {
|
||||
self.emit('position update', self.row, self.column);
|
||||
@@ -190,10 +189,11 @@ function ANSIEscapeParser(options) {
|
||||
|
||||
|
||||
self.emit('mci', {
|
||||
mci : mciCode,
|
||||
id : id ? parseInt(id, 10) : null,
|
||||
args : args,
|
||||
SGR : ansi.getSGRFromGraphicRendition(self.graphicRendition, true)
|
||||
position : [self.row, self.column],
|
||||
mci : mciCode,
|
||||
id : id ? parseInt(id, 10) : null,
|
||||
args : args,
|
||||
SGR : ansi.getSGRFromGraphicRendition(self.graphicRendition, true)
|
||||
});
|
||||
|
||||
if(self.mciReplaceChar.length > 0) {
|
||||
@@ -215,6 +215,9 @@ function ANSIEscapeParser(options) {
|
||||
}
|
||||
|
||||
self.reset = function(input) {
|
||||
self.column = 1;
|
||||
self.row = Math.min(options?.startRow ?? 1, self.termHeight);
|
||||
|
||||
self.parseState = {
|
||||
// ignore anything past EOF marker, if any
|
||||
buffer : input.split(String.fromCharCode(0x1a), 1)[0],
|
||||
|
||||
@@ -309,7 +309,8 @@ const FONT_ALIAS_TO_SYNCTERM_MAP = {
|
||||
|
||||
'mo_soul' : 'mo_soul',
|
||||
'mosoul' : 'mo_soul',
|
||||
'mO\'sOul' : 'mo_soul',
|
||||
'mo\'soul' : 'mo_soul',
|
||||
'amiga_mosoul' : 'mo_soul',
|
||||
|
||||
'amiga_microknight' : 'microknight',
|
||||
'amiga_microknight+' : 'microknight_plus',
|
||||
|
||||
29
core/art.js
29
core/art.js
@@ -269,19 +269,16 @@ function display(client, art, options, cb) {
|
||||
termHeight : client.term.termHeight,
|
||||
termWidth : client.term.termWidth,
|
||||
trailingLF : options.trailingLF,
|
||||
startRow : options.startRow,
|
||||
});
|
||||
|
||||
let parseComplete = false;
|
||||
let cprListener;
|
||||
let mciMap;
|
||||
const mciCprQueue = [];
|
||||
let artHash;
|
||||
let mciMapFromCache;
|
||||
|
||||
function completed() {
|
||||
if(cprListener) {
|
||||
client.removeListener('cursor position report', cprListener);
|
||||
}
|
||||
|
||||
if(!options.disableMciCache && !mciMapFromCache) {
|
||||
// cache our MCI findings...
|
||||
@@ -314,18 +311,6 @@ function display(client, art, options, cb) {
|
||||
// no cached MCI info
|
||||
mciMap = {};
|
||||
|
||||
cprListener = function(pos) {
|
||||
if(mciCprQueue.length > 0) {
|
||||
mciMap[mciCprQueue.shift()].position = pos;
|
||||
|
||||
if(parseComplete && 0 === mciCprQueue.length) {
|
||||
return completed();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
client.on('cursor position report', cprListener);
|
||||
|
||||
let generatedId = 100;
|
||||
|
||||
ansiParser.on('mci', mciInfo => {
|
||||
@@ -339,18 +324,16 @@ function display(client, art, options, cb) {
|
||||
mapEntry.focusArgs = mciInfo.args;
|
||||
} else {
|
||||
mciMap[mapKey] = {
|
||||
args : mciInfo.args,
|
||||
SGR : mciInfo.SGR,
|
||||
code : mciInfo.mci,
|
||||
id : id,
|
||||
position : mciInfo.position,
|
||||
args : mciInfo.args,
|
||||
SGR : mciInfo.SGR,
|
||||
code : mciInfo.mci,
|
||||
id : id,
|
||||
};
|
||||
|
||||
if(!mciInfo.id) {
|
||||
++generatedId;
|
||||
}
|
||||
|
||||
mciCprQueue.push(mapKey);
|
||||
client.term.rawWrite(ansi.queryPos());
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ function ButtonView(options) {
|
||||
util.inherits(ButtonView, TextView);
|
||||
|
||||
ButtonView.prototype.onKeyPress = function(ch, key) {
|
||||
if(this.isKeyMapped('accept', key.name) || ' ' === ch) {
|
||||
if(this.isKeyMapped('accept', (key ? key.name : ch)) || ' ' === ch) {
|
||||
this.submitData = 'accept';
|
||||
this.emit('action', 'accept');
|
||||
delete this.submitData;
|
||||
@@ -29,16 +29,6 @@ ButtonView.prototype.onKeyPress = function(ch, key) {
|
||||
ButtonView.super_.prototype.onKeyPress.call(this, ch, key);
|
||||
}
|
||||
};
|
||||
/*
|
||||
ButtonView.prototype.onKeyPress = function(ch, key) {
|
||||
// allow space = submit
|
||||
if(' ' === ch) {
|
||||
this.emit('action', 'accept');
|
||||
}
|
||||
|
||||
ButtonView.super_.prototype.onKeyPress.call(this, ch, key);
|
||||
};
|
||||
*/
|
||||
|
||||
ButtonView.prototype.getData = function() {
|
||||
return this.submitData || null;
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
// ENiGMA½
|
||||
var Log = require('./logger.js').log;
|
||||
var renegadeToAnsi = require('./color_codes.js').renegadeToAnsi;
|
||||
|
||||
const Config = require('./config.js').get;
|
||||
var iconv = require('iconv-lite');
|
||||
var assert = require('assert');
|
||||
var _ = require('lodash');
|
||||
|
||||
|
||||
exports.ClientTerminal = ClientTerminal;
|
||||
|
||||
function ClientTerminal(output) {
|
||||
@@ -115,7 +116,8 @@ ClientTerminal.prototype.isNixTerm = function() {
|
||||
return true;
|
||||
}
|
||||
|
||||
return [ 'xterm', 'linux', 'screen', 'dumb', 'rxvt', 'konsole', 'gnome', 'x11 terminal emulator' ].includes(this.termType);
|
||||
const utf8TermList = Config().term.utf8TermList;
|
||||
return utf8TermList.includes(this.termType);
|
||||
};
|
||||
|
||||
ClientTerminal.prototype.isANSI = function() {
|
||||
@@ -153,7 +155,8 @@ ClientTerminal.prototype.isANSI = function() {
|
||||
// linux:
|
||||
// * JuiceSSH (note: TERM=linux also)
|
||||
//
|
||||
return [ 'ansi', 'pcansi', 'pc-ansi', 'ansi-bbs', 'qansi', 'scoansi', 'syncterm', 'ansi-256color' ].includes(this.termType);
|
||||
const cp437TermList = Config().term.cp437TermList;
|
||||
return cp437TermList.includes(this.termType);
|
||||
};
|
||||
|
||||
// :TODO: probably need to update these to convert IAC (0xff) -> IACIAC (escape it)
|
||||
|
||||
@@ -126,7 +126,7 @@ function renegadeToAnsi(s, client) {
|
||||
|
||||
//
|
||||
// Converts various control codes popular in BBS packages
|
||||
// to ANSI escape sequences. Additionaly supports ENiGMA style
|
||||
// to ANSI escape sequences. Additionally supports ENiGMA style
|
||||
// MCI codes.
|
||||
//
|
||||
// Supported control code formats:
|
||||
@@ -134,16 +134,17 @@ function renegadeToAnsi(s, client) {
|
||||
// * PCBoard : @X## where the first number/char is BG color, and second is FG
|
||||
// * WildCat! : @##@ the same as PCBoard without the X prefix, but with a @ suffix
|
||||
// * WWIV : ^#
|
||||
// * CNET Y-Style : 0x19## where ## is a specific set of codes -- this is the older format
|
||||
// * CNET Q-style : 0x11##} where ## is a specific set of codes -- this is the newer format
|
||||
// * CNET Control-Y: AKA Y-Style -- 0x19## where ## is a specific set of codes (older format)
|
||||
// * CNET Control-Q: AKA Q-style -- 0x11##} where ## is a specific set of codes (newer format)
|
||||
//
|
||||
// TODO: Add Synchronet and Celerity format support
|
||||
//
|
||||
// Resources:
|
||||
// * http://wiki.synchro.net/custom:colors
|
||||
// * https://archive.org/stream/C-Net_Pro_3.0_1994_Perspective_Software/C-Net_Pro_3.0_1994_Perspective_Software_djvu.txt
|
||||
//
|
||||
function controlCodesToAnsi(s, client) {
|
||||
const RE = /(\|([A-Z0-9]{2})|\|)|(@X([0-9A-F]{2}))|(@([0-9A-F]{2})@)|(\x03[0-9]|\x03)|(\x19(c[0-9a-f]|z[0-7]|n1|f1)|\x19)|(\x11(c[0-9a-f]|z[0-7]|n1|f1)}|\x11)/g; // eslint-disable-line no-control-regex
|
||||
const RE = /(\|([A-Z0-9]{2})|\|)|(@X([0-9A-F]{2}))|(@([0-9A-F]{2})@)|(\x03[0-9]|\x03)|(\x19(c[0-9a-f]|z[0-7]|n1|f1|q1)|\x19)|(\x11(c[0-9a-f]|z[0-7]|n1|f1|q1)}|\x11)/g; // eslint-disable-line no-control-regex
|
||||
|
||||
let m;
|
||||
let result = '';
|
||||
|
||||
@@ -17,6 +17,24 @@ module.exports = () => {
|
||||
achievementFile : 'achievements.hjson',
|
||||
},
|
||||
|
||||
term : {
|
||||
// checkUtf8Encoding requires the use of cursor position reports, which are not supported on all terminals.
|
||||
// Using this with a terminal that does not support cursor position reports results in a 2 second delay
|
||||
// during the connect process, but provides better autoconfiguration of utf-8
|
||||
checkUtf8Encoding : true,
|
||||
|
||||
// Checking the ANSI home position also requires the use of cursor position reports, which are not
|
||||
// supported on all terminals. Using this with a terminal that does not support cursor position reports
|
||||
// results in a 3 second delay during the connect process, but works around positioning problems with
|
||||
// non-standard terminals.
|
||||
checkAnsiHomePosition: true,
|
||||
|
||||
// List of terms that should be assumed to use cp437 encoding
|
||||
cp437TermList : ['ansi', 'pcansi', 'pc-ansi', 'ansi-bbs', 'qansi', 'scoansi', 'syncterm', 'ansi-256color', 'ansi-256color-rgb'],
|
||||
// List of terms that should be assumed to use utf8 encoding
|
||||
utf8TermList : ['xterm', 'linux', 'screen', 'dumb', 'rxvt', 'konsole', 'gnome', 'x11 terminal emulator'],
|
||||
},
|
||||
|
||||
users : {
|
||||
usernameMin : 2,
|
||||
usernameMax : 16, // Note that FidoNet wants 36 max
|
||||
@@ -166,10 +184,11 @@ module.exports = () => {
|
||||
'ecdh-sha2-nistp256',
|
||||
'ecdh-sha2-nistp384',
|
||||
'ecdh-sha2-nistp521',
|
||||
'diffie-hellman-group-exchange-sha256',
|
||||
'diffie-hellman-group14-sha1',
|
||||
'diffie-hellman-group-exchange-sha1',
|
||||
'diffie-hellman-group1-sha1',
|
||||
// Group exchange not currnetly supported
|
||||
// 'diffie-hellman-group-exchange-sha256',
|
||||
// 'diffie-hellman-group-exchange-sha1',
|
||||
],
|
||||
cipher : [
|
||||
'aes128-ctr',
|
||||
@@ -263,7 +282,7 @@ module.exports = () => {
|
||||
port : 8070,
|
||||
publicHostname : 'another-fine-enigma-bbs.org',
|
||||
publicPort : 8070, // adjust if behind NAT/etc.
|
||||
bannerFile : 'gopher_banner.asc',
|
||||
staticRoot : paths.join(__dirname, './../gopher'),
|
||||
|
||||
//
|
||||
// Set messageConferences{} to maps of confTag -> [ areaTag1, areaTag2, ... ]
|
||||
@@ -492,7 +511,7 @@ module.exports = () => {
|
||||
},
|
||||
decompress : {
|
||||
cmd : '7za',
|
||||
args : [ 'e', '-o{extractPath}', '{archivePath}' ] // :TODO: should be 'x'?
|
||||
args : [ 'e', '-y', '-o{extractPath}', '{archivePath}' ] // :TODO: should be 'x'?
|
||||
},
|
||||
list : {
|
||||
cmd : '7za',
|
||||
@@ -501,7 +520,7 @@ module.exports = () => {
|
||||
},
|
||||
extract : {
|
||||
cmd : '7za',
|
||||
args : [ 'e', '-o{extractPath}', '{archivePath}', '{fileList}' ],
|
||||
args : [ 'e', '-y', '-o{extractPath}', '{archivePath}', '{fileList}' ],
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// ENiGMA½
|
||||
const ansi = require('./ansi_term.js');
|
||||
const Events = require('./events.js');
|
||||
const Config = require('./config.js').get;
|
||||
const { Errors } = require('./enig_error.js');
|
||||
|
||||
// deps
|
||||
@@ -18,6 +19,13 @@ function ansiDiscoverHomePosition(client, cb) {
|
||||
// think of home as 0,0. If this is the case, we need to offset
|
||||
// our positioning to accommodate for such.
|
||||
//
|
||||
|
||||
|
||||
if( !Config().term.checkAnsiHomePosition ) {
|
||||
// Skip (and assume 1,1) if the home position check is disabled.
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
const done = (err) => {
|
||||
client.removeListener('cursor position report', cprListener);
|
||||
clearTimeout(giveUpTimer);
|
||||
@@ -68,8 +76,9 @@ function ansiAttemptDetectUTF8(client, cb) {
|
||||
//
|
||||
// We currently only do this if the term hasn't already been ID'd as a
|
||||
// "*nix" terminal -- that is, xterm, etc.
|
||||
//
|
||||
if(!client.term.isNixTerm()) {
|
||||
// Also skip this check if checkUtf8Encoding is disabled in the config
|
||||
|
||||
if(!client.term.isNixTerm() || !Config().term.checkUtf8Encoding) {
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
@@ -119,6 +128,8 @@ function ansiAttemptDetectUTF8(client, cb) {
|
||||
return giveUp();
|
||||
}, 2000);
|
||||
|
||||
|
||||
|
||||
client.once('cursor position report', cprListener);
|
||||
client.term.rawWrite(ansi.goHome() + ansi.queryPos());
|
||||
}
|
||||
@@ -199,7 +210,7 @@ function displayBanner(term) {
|
||||
// note: intentional formatting:
|
||||
term.pipeWrite(`
|
||||
|06Connected to |02EN|10i|02GMA|10½ |06BBS version |12|VN
|
||||
|06Copyright (c) 2014-2020 Bryan Ashby |14- |12http://l33t.codes/
|
||||
|06Copyright (c) 2014-2022 Bryan Ashby |14- |12http://l33t.codes/
|
||||
|06Updates & source |14- |12https://github.com/NuSkooler/enigma-bbs/
|
||||
|00`
|
||||
);
|
||||
@@ -216,7 +227,6 @@ function connectEntry(client, nextMenu) {
|
||||
},
|
||||
function discoverHomePosition(callback) {
|
||||
ansiDiscoverHomePosition(client, () => {
|
||||
// :TODO: If CPR for home fully fails, we should bail out on the connection with an error, e.g. ANSI support required
|
||||
return callback(null); // we try to continue anyway
|
||||
});
|
||||
},
|
||||
|
||||
@@ -421,7 +421,8 @@ const DB_INIT_TABLE = {
|
||||
hash_tag_id INTEGER NOT NULL,
|
||||
file_id INTEGER NOT NULL,
|
||||
|
||||
UNIQUE(hash_tag_id, file_id)
|
||||
UNIQUE(hash_tag_id, file_id),
|
||||
FOREIGN KEY(file_id) REFERENCES file(file_id) ON DELETE CASCADE
|
||||
);`
|
||||
);
|
||||
|
||||
@@ -431,7 +432,10 @@ const DB_INIT_TABLE = {
|
||||
user_id INTEGER NOT NULL,
|
||||
rating INTEGER NOT NULL,
|
||||
|
||||
UNIQUE(file_id, user_id)
|
||||
UNIQUE(file_id, user_id),
|
||||
FOREIGN KEY(file_id) REFERENCES file(file_id) ON DELETE CASCADE
|
||||
-- Note that we cannot CASCADE if user_id is removed from user.db
|
||||
-- See processing in oputil's removeUser()
|
||||
);`
|
||||
);
|
||||
|
||||
@@ -447,7 +451,8 @@ const DB_INIT_TABLE = {
|
||||
hash_id VARCHAR NOT NULL,
|
||||
file_id INTEGER NOT NULL,
|
||||
|
||||
UNIQUE(hash_id, file_id)
|
||||
UNIQUE(hash_id, file_id),
|
||||
FOREIGN KEY(file_id) REFERENCES file(file_id) ON DELETE CASCADE
|
||||
);`
|
||||
);
|
||||
|
||||
|
||||
@@ -6,27 +6,45 @@ const TextView = require('./text_view.js').TextView;
|
||||
const miscUtil = require('./misc_util.js');
|
||||
const strUtil = require('./string_util.js');
|
||||
|
||||
const VIEW_SPECIAL_KEY_MAP_DEFAULT = require('./view').VIEW_SPECIAL_KEY_MAP_DEFAULT;
|
||||
|
||||
// deps
|
||||
const _ = require('lodash');
|
||||
|
||||
exports.EditTextView = EditTextView;
|
||||
|
||||
const EDIT_TEXT_VIEW_KEY_MAP = Object.assign({}, VIEW_SPECIAL_KEY_MAP_DEFAULT, {
|
||||
delete : [ 'delete', 'ctrl + d' ], // https://www.tecmint.com/linux-command-line-bash-shortcut-keys/
|
||||
});
|
||||
|
||||
function EditTextView(options) {
|
||||
options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true);
|
||||
options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true);
|
||||
options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block');
|
||||
options.resizable = false;
|
||||
|
||||
if(!_.isObject(options.specialKeyMap)) {
|
||||
options.specialKeyMap = EDIT_TEXT_VIEW_KEY_MAP;
|
||||
}
|
||||
|
||||
TextView.call(this, options);
|
||||
|
||||
this.initDefaultWidth();
|
||||
|
||||
this.cursorPos = { row : 0, col : 0 };
|
||||
|
||||
this.clientBackspace = function() {
|
||||
const fillCharSGR = this.getStyleSGR(1) || this.getSGR();
|
||||
this.client.term.write(`\b${fillCharSGR}${this.fillChar}\b${this.getFocusSGR()}`);
|
||||
};
|
||||
this.text = this.text.substr(0, this.text.length - 1);
|
||||
|
||||
if(this.text.length >= this.dimens.width) {
|
||||
this.redraw();
|
||||
} else {
|
||||
this.cursorPos.col -= 1;
|
||||
if(this.cursorPos.col >= 0) {
|
||||
const fillCharSGR = this.getStyleSGR(1) || this.getSGR();
|
||||
this.client.term.write(`\b${fillCharSGR}${this.fillChar}\b${this.getFocusSGR()}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
require('util').inherits(EditTextView, TextView);
|
||||
@@ -35,19 +53,16 @@ EditTextView.prototype.onKeyPress = function(ch, key) {
|
||||
if(key) {
|
||||
if(this.isKeyMapped('backspace', key.name)) {
|
||||
if(this.text.length > 0) {
|
||||
this.text = this.text.substr(0, this.text.length - 1);
|
||||
|
||||
if(this.text.length >= this.dimens.width) {
|
||||
this.redraw();
|
||||
} else {
|
||||
this.cursorPos.col -= 1;
|
||||
if(this.cursorPos.col >= 0) {
|
||||
this.clientBackspace();
|
||||
}
|
||||
}
|
||||
this.clientBackspace();
|
||||
}
|
||||
|
||||
return EditTextView.super_.prototype.onKeyPress.call(this, ch, key);
|
||||
} else if (this.isKeyMapped('delete', key.name)) {
|
||||
// Some (mostly older) terms send 'delete' for Backspace.
|
||||
// if we're at the end of the line, go ahead and treat them the same
|
||||
if (this.text.length > 0 && this.cursorPos.col === this.text.length) {
|
||||
this.clientBackspace();
|
||||
}
|
||||
} else if(this.isKeyMapped('clearLine', key.name)) {
|
||||
this.text = '';
|
||||
this.cursorPos.col = 0;
|
||||
|
||||
@@ -144,7 +144,10 @@ exports.getModule = class FileAreaList extends MenuModule {
|
||||
},
|
||||
displayHelp : (formData, extraArgs, cb) => {
|
||||
return this.displayHelpPage(cb);
|
||||
}
|
||||
},
|
||||
movementKeyPressed : (formData, extraArgs, cb) => {
|
||||
return this._handleMovementKeyPress(_.get(formData, 'key.name'), cb);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -201,7 +204,7 @@ exports.getModule = class FileAreaList extends MenuModule {
|
||||
},
|
||||
function display(callback) {
|
||||
return self.displayBrowsePage(false, err => {
|
||||
if(err && 'NORESULTS' === err.reasonCode) {
|
||||
if(err) {
|
||||
self.gotoMenu(self.menuConfig.config.noResultsMenu || 'fileBaseListEntriesNoResults');
|
||||
}
|
||||
return callback(err);
|
||||
@@ -505,6 +508,23 @@ exports.getModule = class FileAreaList extends MenuModule {
|
||||
);
|
||||
}
|
||||
|
||||
_handleMovementKeyPress(keyName, cb) {
|
||||
const descView = this.viewControllers.browse.getView(MciViewIds.browse.desc);
|
||||
if (!descView) {
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
switch (keyName) {
|
||||
case 'down arrow' : descView.scrollDocumentUp(); break;
|
||||
case 'up arrow' : descView.scrollDocumentDown(); break;
|
||||
case 'page up' : descView.keyPressPageUp(); break;
|
||||
case 'page down' : descView.keyPressPageDown(); break;
|
||||
}
|
||||
|
||||
this.viewControllers.browse.switchFocus(MciViewIds.browse.navMenu);
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
fetchAndDisplayWebDownloadLink(cb) {
|
||||
const self = this;
|
||||
|
||||
@@ -720,7 +740,7 @@ exports.getModule = class FileAreaList extends MenuModule {
|
||||
}
|
||||
|
||||
FileEntry.findFiles(filterCriteria, (err, fileIds) => {
|
||||
this.fileList = fileIds;
|
||||
this.fileList = fileIds || [];
|
||||
return cb(err);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ exports.getFileAreasByTagWildcardRule = getFileAreasByTagWildcardRule;
|
||||
exports.getFileEntryPath = getFileEntryPath;
|
||||
exports.changeFileAreaWithOptions = changeFileAreaWithOptions;
|
||||
exports.scanFile = scanFile;
|
||||
exports.scanFileAreaForChanges = scanFileAreaForChanges;
|
||||
//exports.scanFileAreaForChanges = scanFileAreaForChanges;
|
||||
exports.getDescFromFileName = getDescFromFileName;
|
||||
exports.getAreaStats = getAreaStats;
|
||||
exports.cleanUpTempSessionItems = cleanUpTempSessionItems;
|
||||
@@ -139,7 +139,14 @@ function getDefaultFileAreaTag(client, disableAcsCheck) {
|
||||
function getFileAreaByTag(areaTag) {
|
||||
const areaInfo = Config().fileBase.areas[areaTag];
|
||||
if(areaInfo) {
|
||||
areaInfo.areaTag = areaTag; // convienence!
|
||||
// normalize |hashTags|
|
||||
if (_.isString(areaInfo.hashTags)) {
|
||||
areaInfo.hashTags = areaInfo.hashTags.trim().split(',');
|
||||
}
|
||||
if (Array.isArray(areaInfo.hashTags)) {
|
||||
areaInfo.hashTags = new Set(areaInfo.hashTags.map(t => t.trim()));
|
||||
}
|
||||
areaInfo.areaTag = areaTag; // convenience!
|
||||
areaInfo.storage = getAreaStorageLocations(areaInfo);
|
||||
return areaInfo;
|
||||
}
|
||||
@@ -794,7 +801,7 @@ function scanFile(filePath, options, iterator, cb) {
|
||||
stepInfo.calcHashPercent = Math.round(((stepInfo.bytesProcessed / stepInfo.byteSize) * 100));
|
||||
|
||||
//
|
||||
// Only send 'hash_update' step update if we have a noticable percentage change in progress
|
||||
// Only send 'hash_update' step update if we have a noticeable percentage change in progress
|
||||
//
|
||||
const data = bytesRead < chunkSize ? buffer.slice(0, bytesRead) : buffer;
|
||||
if(!iterator || stepInfo.calcHashPercent === lastCalcHashPercent) {
|
||||
@@ -871,90 +878,91 @@ function scanFile(filePath, options, iterator, cb) {
|
||||
);
|
||||
}
|
||||
|
||||
function scanFileAreaForChanges(areaInfo, options, iterator, cb) {
|
||||
if(3 === arguments.length && _.isFunction(iterator)) {
|
||||
cb = iterator;
|
||||
iterator = null;
|
||||
} else if(2 === arguments.length && _.isFunction(options)) {
|
||||
cb = options;
|
||||
iterator = null;
|
||||
options = {};
|
||||
}
|
||||
// :TODO: this stuff needs cleaned up
|
||||
// function scanFileAreaForChanges(areaInfo, options, iterator, cb) {
|
||||
// if(3 === arguments.length && _.isFunction(iterator)) {
|
||||
// cb = iterator;
|
||||
// iterator = null;
|
||||
// } else if(2 === arguments.length && _.isFunction(options)) {
|
||||
// cb = options;
|
||||
// iterator = null;
|
||||
// options = {};
|
||||
// }
|
||||
|
||||
const storageLocations = getAreaStorageLocations(areaInfo);
|
||||
// const storageLocations = getAreaStorageLocations(areaInfo);
|
||||
|
||||
async.eachSeries(storageLocations, (storageLoc, nextLocation) => {
|
||||
async.series(
|
||||
[
|
||||
function scanPhysFiles(callback) {
|
||||
const physDir = storageLoc.dir;
|
||||
// async.eachSeries(storageLocations, (storageLoc, nextLocation) => {
|
||||
// async.series(
|
||||
// [
|
||||
// function scanPhysFiles(callback) {
|
||||
// const physDir = storageLoc.dir;
|
||||
|
||||
fs.readdir(physDir, (err, files) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
// fs.readdir(physDir, (err, files) => {
|
||||
// if(err) {
|
||||
// return callback(err);
|
||||
// }
|
||||
|
||||
async.eachSeries(files, (fileName, nextFile) => {
|
||||
const fullPath = paths.join(physDir, fileName);
|
||||
// async.eachSeries(files, (fileName, nextFile) => {
|
||||
// const fullPath = paths.join(physDir, fileName);
|
||||
|
||||
fs.stat(fullPath, (err, stats) => {
|
||||
if(err) {
|
||||
// :TODO: Log me!
|
||||
return nextFile(null); // always try next file
|
||||
}
|
||||
// fs.stat(fullPath, (err, stats) => {
|
||||
// if(err) {
|
||||
// // :TODO: Log me!
|
||||
// return nextFile(null); // always try next file
|
||||
// }
|
||||
|
||||
if(!stats.isFile()) {
|
||||
return nextFile(null);
|
||||
}
|
||||
// if(!stats.isFile()) {
|
||||
// return nextFile(null);
|
||||
// }
|
||||
|
||||
scanFile(
|
||||
fullPath,
|
||||
{
|
||||
areaTag : areaInfo.areaTag,
|
||||
storageTag : storageLoc.storageTag
|
||||
},
|
||||
iterator,
|
||||
(err, fileEntry, dupeEntries) => {
|
||||
if(err) {
|
||||
// :TODO: Log me!!!
|
||||
return nextFile(null); // try next anyway
|
||||
}
|
||||
// scanFile(
|
||||
// fullPath,
|
||||
// {
|
||||
// areaTag : areaInfo.areaTag,
|
||||
// storageTag : storageLoc.storageTag
|
||||
// },
|
||||
// iterator,
|
||||
// (err, fileEntry, dupeEntries) => {
|
||||
// if(err) {
|
||||
// // :TODO: Log me!!!
|
||||
// return nextFile(null); // try next anyway
|
||||
// }
|
||||
|
||||
if(dupeEntries.length > 0) {
|
||||
// :TODO: Handle duplidates -- what to do here???
|
||||
} else {
|
||||
if(Array.isArray(options.tags)) {
|
||||
options.tags.forEach(tag => {
|
||||
fileEntry.hashTags.add(tag);
|
||||
});
|
||||
}
|
||||
addNewFileEntry(fileEntry, fullPath, err => {
|
||||
// pass along error; we failed to insert a record in our DB or something else bad
|
||||
return nextFile(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}, err => {
|
||||
return callback(err);
|
||||
});
|
||||
});
|
||||
},
|
||||
function scanDbEntries(callback) {
|
||||
// :TODO: Look @ db entries for area that were *not* processed above
|
||||
return callback(null);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
return nextLocation(err);
|
||||
}
|
||||
);
|
||||
},
|
||||
err => {
|
||||
return cb(err);
|
||||
});
|
||||
}
|
||||
// if(dupeEntries.length > 0) {
|
||||
// // :TODO: Handle duplicates -- what to do here???
|
||||
// } else {
|
||||
// if(Array.isArray(options.tags)) {
|
||||
// options.tags.forEach(tag => {
|
||||
// fileEntry.hashTags.add(tag);
|
||||
// });
|
||||
// }
|
||||
// addNewFileEntry(fileEntry, fullPath, err => {
|
||||
// // pass along error; we failed to insert a record in our DB or something else bad
|
||||
// return nextFile(err);
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
// });
|
||||
// }, err => {
|
||||
// return callback(err);
|
||||
// });
|
||||
// });
|
||||
// },
|
||||
// function scanDbEntries(callback) {
|
||||
// // :TODO: Look @ db entries for area that were *not* processed above
|
||||
// return callback(null);
|
||||
// }
|
||||
// ],
|
||||
// err => {
|
||||
// return nextLocation(err);
|
||||
// }
|
||||
// );
|
||||
// },
|
||||
// err => {
|
||||
// return cb(err);
|
||||
// });
|
||||
// }
|
||||
|
||||
function getDescFromFileName(fileName) {
|
||||
//
|
||||
|
||||
@@ -243,6 +243,15 @@ module.exports = class FileEntry {
|
||||
);
|
||||
}
|
||||
|
||||
static removeUserRatings(userId, cb) {
|
||||
return fileDb.run(
|
||||
`DELETE FROM file_user_rating
|
||||
WHERE user_id = ?;`,
|
||||
[ userId ],
|
||||
cb
|
||||
);
|
||||
}
|
||||
|
||||
static persistMetaValue(fileId, name, value, transOrDb, cb) {
|
||||
if(!_.isFunction(cb) && _.isFunction(transOrDb)) {
|
||||
cb = transOrDb;
|
||||
@@ -457,6 +466,16 @@ module.exports = class FileEntry {
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
// Find file(s) by |filter|
|
||||
//
|
||||
// - sort: sort results by any well known name, file_id, or user_rating
|
||||
// - terms: one or more search terms to search within filenames as well
|
||||
// as short and long descriptions. We attempt to use the FTS ability when
|
||||
// possible, but want to allow users to search for wildcard matches in
|
||||
// which some cases we'll use multiple LIKE queries.
|
||||
// See _normalizeFileSearchTerms()
|
||||
//
|
||||
static findFiles(filter, cb) {
|
||||
filter = filter || {};
|
||||
|
||||
@@ -500,8 +519,8 @@ module.exports = class FileEntry {
|
||||
if('user_rating' === filter.sort) {
|
||||
sql =
|
||||
`SELECT DISTINCT f.file_id,
|
||||
(SELECT IFNULL(AVG(rating), 0) rating
|
||||
FROM file_user_rating
|
||||
(SELECT IFNULL(AVG(rating), 0) rating
|
||||
FROM file_user_rating
|
||||
WHERE file_id = f.file_id)
|
||||
AS avg_rating
|
||||
FROM file f`;
|
||||
@@ -540,16 +559,16 @@ module.exports = class FileEntry {
|
||||
mp.value = mp.value.replace(/\*/g, '%').replace(/\?/g, '_');
|
||||
appendWhereClause(
|
||||
`f.file_id IN (
|
||||
SELECT file_id
|
||||
FROM file_meta
|
||||
SELECT file_id
|
||||
FROM file_meta
|
||||
WHERE meta_name = "${mp.name}" AND meta_value LIKE "${mp.value}"
|
||||
)`
|
||||
);
|
||||
} else {
|
||||
appendWhereClause(
|
||||
`f.file_id IN (
|
||||
SELECT file_id
|
||||
FROM file_meta
|
||||
SELECT file_id
|
||||
FROM file_meta
|
||||
WHERE meta_name = "${mp.name}" AND meta_value = "${mp.value}"
|
||||
)`
|
||||
);
|
||||
@@ -562,14 +581,29 @@ module.exports = class FileEntry {
|
||||
}
|
||||
|
||||
if(filter.terms && filter.terms.length > 0) {
|
||||
// note the ':' in MATCH expr., see https://www.sqlite.org/cvstrac/wiki?p=FullTextIndex
|
||||
appendWhereClause(
|
||||
`f.file_id IN (
|
||||
SELECT rowid
|
||||
FROM file_fts
|
||||
WHERE file_fts MATCH ":${sanitizeString(filter.terms)}"
|
||||
)`
|
||||
);
|
||||
const [terms, queryType] = FileEntry._normalizeFileSearchTerms(filter.terms);
|
||||
|
||||
if ('fts_match' === queryType) {
|
||||
// note the ':' in MATCH expr., see https://www.sqlite.org/cvstrac/wiki?p=FullTextIndex
|
||||
appendWhereClause(
|
||||
`f.file_id IN (
|
||||
SELECT rowid
|
||||
FROM file_fts
|
||||
WHERE file_fts MATCH ":${terms}"
|
||||
)`
|
||||
);
|
||||
} else {
|
||||
appendWhereClause(
|
||||
`(f.file_name LIKE "${terms}" OR
|
||||
f.desc LIKE "${terms}" OR
|
||||
f.desc_long LIKE "${terms}")`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// handle e.g. 1998 -> "1998"
|
||||
if (_.isNumber(filter.tags)) {
|
||||
filter.tags = filter.tags.toString();
|
||||
}
|
||||
|
||||
if(filter.tags && filter.tags.length > 0) {
|
||||
@@ -693,4 +727,46 @@ module.exports = class FileEntry {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
static _normalizeFileSearchTerms(terms) {
|
||||
// ensure we have reasonable input to start with
|
||||
terms = sanitizeString(terms.toString());
|
||||
|
||||
// No wildcards?
|
||||
const hasSingleCharWC = terms.indexOf('?') > -1;
|
||||
if (terms.indexOf('*') === -1 && !hasSingleCharWC) {
|
||||
return [ terms, 'fts_match' ];
|
||||
}
|
||||
|
||||
const prepareLike = () => {
|
||||
// Convert * and ? to SQL LIKE style
|
||||
terms = terms.replace(/\*/g, '%').replace(/\?/g, '_');
|
||||
return terms;
|
||||
};
|
||||
|
||||
// Any ? wildcards?
|
||||
if (hasSingleCharWC) {
|
||||
return [ prepareLike(terms), 'like' ];
|
||||
}
|
||||
|
||||
const split = terms.replace(/\s+/g, ' ').split(' ');
|
||||
const useLike = split.some(term => {
|
||||
if (term.indexOf('?') > -1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const wcPos = term.indexOf('*');
|
||||
if (wcPos > -1 && wcPos !== term.length - 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (useLike) {
|
||||
return [ prepareLike(terms), 'like' ];
|
||||
}
|
||||
|
||||
return [ terms, 'fts_match' ];
|
||||
}
|
||||
};
|
||||
|
||||
167
core/fse.js
167
core/fse.js
@@ -21,7 +21,10 @@ const {
|
||||
isAnsi, stripAnsiControlCodes,
|
||||
insert
|
||||
} = require('./string_util.js');
|
||||
const { stripMciColorCodes } = require('./color_codes.js');
|
||||
const {
|
||||
stripMciColorCodes,
|
||||
controlCodesToAnsi,
|
||||
} = require('./color_codes.js');
|
||||
const Config = require('./config.js').get;
|
||||
const { getAddressedToInfo } = require('./mail_util.js');
|
||||
const Events = require('./events.js');
|
||||
@@ -418,7 +421,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
|
||||
//
|
||||
// Find tearline - we want to color it differently.
|
||||
//
|
||||
const tearLinePos = this.message.getTearLinePosition(msg);
|
||||
const tearLinePos = Message.getTearLinePosition(msg);
|
||||
|
||||
if(tearLinePos > -1) {
|
||||
msg = insert(msg, tearLinePos, bodyMessageView.getSGRFor('text'));
|
||||
@@ -432,7 +435,55 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
|
||||
}
|
||||
);
|
||||
} else {
|
||||
bodyMessageView.setText(stripAnsiControlCodes(msg));
|
||||
msg = stripAnsiControlCodes(msg); // start clean
|
||||
|
||||
const styleToArray = (style, len) => {
|
||||
if (!Array.isArray(style)) {
|
||||
style = [ style ];
|
||||
}
|
||||
while (style.length < len) {
|
||||
style.push(style[0]);
|
||||
}
|
||||
return style;
|
||||
};
|
||||
|
||||
//
|
||||
// In *View* mode, if enabled, do a little prep work so we can stylize:
|
||||
// - Quote indicators
|
||||
// - Tear lines
|
||||
// - Origins
|
||||
//
|
||||
if (this.menuConfig.config.quoteStyleLevel1) {
|
||||
// can be a single style to cover 'XX> TEXT' or an array to cover 'XX', '>', and TEXT
|
||||
// Non-standard (as for BBSes) single > TEXT, omitting space before XX, etc. are allowed
|
||||
const styleL1 = styleToArray(this.menuConfig.config.quoteStyleLevel1, 3);
|
||||
|
||||
const QuoteRegex = /^([ ]?)([!-~]{0,2})>([ ]*)([^\r\n]*\r?\n)/gm;
|
||||
msg = msg.replace(QuoteRegex, (m, spc1, initials, spc2, text) => {
|
||||
return `${spc1}${styleL1[0]}${initials}${styleL1[1]}>${spc2}${styleL1[2]}${text}${bodyMessageView.styleSGR1}`;
|
||||
});
|
||||
}
|
||||
|
||||
if (this.menuConfig.config.tearLineStyle) {
|
||||
// '---' and TEXT
|
||||
const style = styleToArray(this.menuConfig.config.tearLineStyle, 2);
|
||||
|
||||
const TearLineRegex = /^--- (.+)$(?![\s\S]*^--- .+$)/m;
|
||||
msg = msg.replace(TearLineRegex, (m, text) => {
|
||||
return `${style[0]}--- ${style[1]}${text}${bodyMessageView.styleSGR1}`;
|
||||
});
|
||||
}
|
||||
|
||||
if (this.menuConfig.config.originStyle) {
|
||||
const style = styleToArray(this.menuConfig.config.originStyle, 3);
|
||||
|
||||
const OriginRegex = /^([ ]{1,2})\* Origin: (.+)$/m;
|
||||
msg = msg.replace(OriginRegex, (m, spc, text) => {
|
||||
return `${spc}${style[0]}* ${style[1]}Origin: ${style[2]}${text}${bodyMessageView.styleSGR1}`;
|
||||
});
|
||||
}
|
||||
|
||||
bodyMessageView.setText(controlCodesToAnsi(msg));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -552,7 +603,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
|
||||
theme.displayThemedAsset(
|
||||
footerArt,
|
||||
self.client,
|
||||
{ font : self.menuConfig.font },
|
||||
{ font : self.menuConfig.font, startRow: self.header.height + self.body.height },
|
||||
function displayed(err, artData) {
|
||||
callback(err, artData);
|
||||
}
|
||||
@@ -575,19 +626,34 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
|
||||
async.series(
|
||||
[
|
||||
function displayHeaderAndBody(callback) {
|
||||
async.eachSeries( comps, function dispArt(n, next) {
|
||||
theme.displayThemedAsset(
|
||||
art[n],
|
||||
self.client,
|
||||
{ font : self.menuConfig.font },
|
||||
function displayed(err) {
|
||||
next(err);
|
||||
async.waterfall(
|
||||
[
|
||||
function displayHeader(callback) {
|
||||
theme.displayThemedAsset(
|
||||
art['header'],
|
||||
self.client,
|
||||
{ font : self.menuConfig.font },
|
||||
function displayed(err, artInfo) {
|
||||
return callback(err, artInfo);
|
||||
}
|
||||
);
|
||||
},
|
||||
function displayBody(artInfo, callback) {
|
||||
theme.displayThemedAsset(
|
||||
art['header'],
|
||||
self.client,
|
||||
{ font : self.menuConfig.font, startRow: artInfo.height + 1 },
|
||||
function displayed(err, artInfo) {
|
||||
return callback(err, artInfo);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}, function complete(err) {
|
||||
//self.body.height = self.client.term.termHeight - self.header.height - 1;
|
||||
callback(err);
|
||||
});
|
||||
],
|
||||
function complete(err) {
|
||||
//self.body.height = self.client.term.termHeight - self.header.height - 1;
|
||||
callback(err);
|
||||
}
|
||||
);
|
||||
},
|
||||
function displayFooter(callback) {
|
||||
// we have to treat the footer special
|
||||
@@ -649,31 +715,39 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
|
||||
|
||||
assert(_.isObject(art));
|
||||
|
||||
async.series(
|
||||
async.waterfall(
|
||||
[
|
||||
function beforeDisplayArt(callback) {
|
||||
self.beforeArt(callback);
|
||||
},
|
||||
function displayHeaderAndBodyArt(callback) {
|
||||
async.eachSeries( [ 'header', 'body' ], function dispArt(n, next) {
|
||||
theme.displayThemedAsset(
|
||||
art[n],
|
||||
self.client,
|
||||
{ font : self.menuConfig.font },
|
||||
function displayed(err, artData) {
|
||||
if(artData) {
|
||||
mciData[n] = artData;
|
||||
self[n] = { height : artData.height };
|
||||
}
|
||||
|
||||
next(err);
|
||||
function displayHeader(callback) {
|
||||
theme.displayThemedAsset(
|
||||
art.header,
|
||||
self.client,
|
||||
{ font : self.menuConfig.font },
|
||||
function displayed(err, artInfo) {
|
||||
if(artInfo) {
|
||||
mciData['header'] = artInfo;
|
||||
self.header = {height: artInfo.height};
|
||||
}
|
||||
);
|
||||
}, function complete(err) {
|
||||
callback(err);
|
||||
});
|
||||
return callback(err, artInfo);
|
||||
}
|
||||
);
|
||||
},
|
||||
function displayFooter(callback) {
|
||||
function displayBody(artInfo, callback) {
|
||||
theme.displayThemedAsset(
|
||||
art.body,
|
||||
self.client,
|
||||
{ font : self.menuConfig.font, startRow: artInfo.height + 1 },
|
||||
function displayed(err, artInfo) {
|
||||
if(artInfo) {
|
||||
mciData['body'] = artInfo;
|
||||
self.body = {height: artInfo.height - self.header.height};
|
||||
}
|
||||
return callback(err, artInfo);
|
||||
});
|
||||
},
|
||||
function displayFooter(artInfo, callback) {
|
||||
self.setInitialFooterMode();
|
||||
|
||||
var footerName = self.getFooterName();
|
||||
@@ -740,10 +814,10 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
|
||||
});
|
||||
},
|
||||
function prepareViewStates(callback) {
|
||||
var header = self.viewControllers.header;
|
||||
var from = header.getView(MciViewIds.header.from);
|
||||
from.acceptsFocus = false;
|
||||
//from.setText(self.client.user.username);
|
||||
let from = self.viewControllers.header.getView(MciViewIds.header.from);
|
||||
if (from) {
|
||||
from.acceptsFocus = false;
|
||||
}
|
||||
|
||||
// :TODO: make this a method
|
||||
var body = self.viewControllers.body.getView(MciViewIds.body.message);
|
||||
@@ -774,10 +848,12 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
|
||||
{
|
||||
const fromView = self.viewControllers.header.getView(MciViewIds.header.from);
|
||||
const area = getMessageAreaByTag(self.messageAreaTag);
|
||||
if(area && area.realNames) {
|
||||
fromView.setText(self.client.user.properties[UserProps.RealName] || self.client.user.username);
|
||||
} else {
|
||||
fromView.setText(self.client.user.username);
|
||||
if(fromView !== undefined) {
|
||||
if(area && area.realNames) {
|
||||
fromView.setText(self.client.user.properties[UserProps.RealName] || self.client.user.username);
|
||||
} else {
|
||||
fromView.setText(self.client.user.username);
|
||||
}
|
||||
}
|
||||
|
||||
if(self.replyToMessage) {
|
||||
@@ -863,7 +939,10 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
|
||||
}
|
||||
|
||||
initHeaderViewMode() {
|
||||
this.setHeaderText(MciViewIds.header.from, this.message.fromUserName);
|
||||
// Only set header text for from view if it is on the form
|
||||
if (this.viewControllers.header.getView(MciViewIds.header.from) !== undefined) {
|
||||
this.setHeaderText(MciViewIds.header.from, this.message.fromUserName);
|
||||
}
|
||||
this.setHeaderText(MciViewIds.header.to, this.message.toUserName);
|
||||
this.setHeaderText(MciViewIds.header.subject, this.message.subject);
|
||||
|
||||
|
||||
511
core/full_menu_view.js
Normal file
511
core/full_menu_view.js
Normal file
@@ -0,0 +1,511 @@
|
||||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const MenuView = require('./menu_view.js').MenuView;
|
||||
const ansi = require('./ansi_term.js');
|
||||
const strUtil = require('./string_util.js');
|
||||
const formatString = require('./string_format');
|
||||
const pipeToAnsi = require('./color_codes.js').pipeToAnsi;
|
||||
|
||||
// deps
|
||||
const util = require('util');
|
||||
const _ = require('lodash');
|
||||
|
||||
exports.FullMenuView = FullMenuView;
|
||||
|
||||
function FullMenuView(options) {
|
||||
options.cursor = options.cursor || 'hide';
|
||||
options.justify = options.justify || 'left';
|
||||
|
||||
|
||||
MenuView.call(this, options);
|
||||
|
||||
|
||||
// Initialize paging
|
||||
this.pages = [];
|
||||
this.currentPage = 0;
|
||||
|
||||
this.initDefaultWidth();
|
||||
|
||||
// we want page up/page down by default
|
||||
if (!_.isObject(options.specialKeyMap)) {
|
||||
Object.assign(this.specialKeyMap, {
|
||||
'page up': ['page up'],
|
||||
'page down': ['page down'],
|
||||
});
|
||||
}
|
||||
|
||||
this.autoAdjustHeightIfEnabled = () => {
|
||||
if (this.autoAdjustHeight) {
|
||||
this.dimens.height = (this.items.length * (this.itemSpacing + 1)) - (this.itemSpacing);
|
||||
this.dimens.height = Math.min(this.dimens.height, this.client.term.termHeight - this.position.row);
|
||||
}
|
||||
|
||||
this.positionCacheExpired = true;
|
||||
};
|
||||
|
||||
this.autoAdjustHeightIfEnabled();
|
||||
|
||||
this.clearPage = () => {
|
||||
let width = this.dimens.width;
|
||||
if (this.oldDimens) {
|
||||
if (this.oldDimens.width > width) {
|
||||
width = this.oldDimens.width;
|
||||
}
|
||||
delete this.oldDimens;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.dimens.height; i++) {
|
||||
const text = `${strUtil.pad(this.fillChar, width, this.fillChar, 'left')}`;
|
||||
this.client.term.write(`${ansi.goto(this.position.row + i, this.position.col)}${this.getSGR()}${text}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.cachePositions = () => {
|
||||
if (this.positionCacheExpired) {
|
||||
// first, clear the page
|
||||
this.clearPage();
|
||||
|
||||
|
||||
this.autoAdjustHeightIfEnabled();
|
||||
|
||||
this.pages = []; // reset
|
||||
|
||||
// Calculate number of items visible per column
|
||||
this.itemsPerRow = Math.floor(this.dimens.height / (this.itemSpacing + 1));
|
||||
// handle case where one can fit at the end
|
||||
if (this.dimens.height > (this.itemsPerRow * (this.itemSpacing + 1))) {
|
||||
this.itemsPerRow++;
|
||||
}
|
||||
|
||||
// Final check to make sure we don't try to display more than we have
|
||||
if (this.itemsPerRow > this.items.length) {
|
||||
this.itemsPerRow = this.items.length;
|
||||
}
|
||||
|
||||
let col = this.position.col;
|
||||
let row = this.position.row;
|
||||
const spacer = new Array(this.itemHorizSpacing + 1).join(this.fillChar);
|
||||
|
||||
let itemInRow = 0;
|
||||
let itemInCol = 0;
|
||||
|
||||
let pageStart = 0;
|
||||
|
||||
for (let i = 0; i < this.items.length; ++i) {
|
||||
itemInRow++;
|
||||
this.items[i].row = row;
|
||||
this.items[i].col = col;
|
||||
this.items[i].itemInRow = itemInRow;
|
||||
|
||||
row += this.itemSpacing + 1;
|
||||
|
||||
// have to calculate the max length on the last entry
|
||||
if (i == this.items.length - 1) {
|
||||
let maxLength = 0;
|
||||
for (let j = 0; j < this.itemsPerRow; j++) {
|
||||
if (this.items[i - j].col != this.items[i].col) {
|
||||
break;
|
||||
}
|
||||
const itemLength = this.items[i - j].text.length;
|
||||
if (itemLength > maxLength) {
|
||||
maxLength = itemLength;
|
||||
}
|
||||
}
|
||||
|
||||
// set length on each item in the column
|
||||
for (let j = 0; j < this.itemsPerRow; j++) {
|
||||
if (this.items[i - j].col != this.items[i].col) {
|
||||
break;
|
||||
}
|
||||
this.items[i - j].fixedLength = maxLength;
|
||||
}
|
||||
|
||||
|
||||
// Check if we have room for this column
|
||||
// skip for column 0, we need at least one
|
||||
if (itemInCol != 0 && (col + maxLength > this.dimens.width)) {
|
||||
// save previous page
|
||||
this.pages.push({ start: pageStart, end: i - itemInRow });
|
||||
|
||||
// fix the last column processed
|
||||
for (let j = 0; j < this.itemsPerRow; j++) {
|
||||
if (this.items[i - j].col != col) {
|
||||
break;
|
||||
}
|
||||
this.items[i - j].col = this.position.col;
|
||||
pageStart = i - j;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Since this is the last page, save the current page as well
|
||||
this.pages.push({ start: pageStart, end: i });
|
||||
|
||||
}
|
||||
// also handle going to next column
|
||||
else if (itemInRow == this.itemsPerRow) {
|
||||
itemInRow = 0;
|
||||
|
||||
// restart row for next column
|
||||
row = this.position.row;
|
||||
let maxLength = 0;
|
||||
for (let j = 0; j < this.itemsPerRow; j++) {
|
||||
// TODO: handle complex items
|
||||
let itemLength = this.items[i - j].text.length;
|
||||
if (itemLength > maxLength) {
|
||||
maxLength = itemLength;
|
||||
}
|
||||
}
|
||||
|
||||
// set length on each item in the column
|
||||
for (let j = 0; j < this.itemsPerRow; j++) {
|
||||
this.items[i - j].fixedLength = maxLength;
|
||||
}
|
||||
|
||||
// Check if we have room for this column in the current page
|
||||
// skip for first column, we need at least one
|
||||
if (itemInCol != 0 && (col + maxLength > this.dimens.width)) {
|
||||
// save previous page
|
||||
this.pages.push({ start: pageStart, end: i - this.itemsPerRow });
|
||||
|
||||
// restart page start for next page
|
||||
pageStart = i - this.itemsPerRow + 1;
|
||||
|
||||
// reset
|
||||
col = this.position.col;
|
||||
itemInRow = 0;
|
||||
|
||||
// fix the last column processed
|
||||
for (let j = 0; j < this.itemsPerRow; j++) {
|
||||
this.items[i - j].col = col;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// increment the column
|
||||
col += maxLength + spacer.length;
|
||||
itemInCol++;
|
||||
}
|
||||
|
||||
|
||||
// Set the current page if the current item is focused.
|
||||
if (this.focusedItemIndex === i) {
|
||||
this.currentPage = this.pages.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.positionCacheExpired = false;
|
||||
};
|
||||
|
||||
this.drawItem = (index) => {
|
||||
const item = this.items[index];
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cached = this.getRenderCacheItem(index, item.focused);
|
||||
if (cached) {
|
||||
return this.client.term.write(`${ansi.goto(item.row, item.col)}${cached}`);
|
||||
}
|
||||
|
||||
let text;
|
||||
let sgr;
|
||||
if (item.focused && this.hasFocusItems()) {
|
||||
const focusItem = this.focusItems[index];
|
||||
text = focusItem ? focusItem.text : item.text;
|
||||
sgr = '';
|
||||
} else if (this.complexItems) {
|
||||
text = pipeToAnsi(formatString(item.focused && this.focusItemFormat ? this.focusItemFormat : this.itemFormat, item));
|
||||
sgr = this.focusItemFormat ? '' : (index === this.focusedItemIndex ? this.getFocusSGR() : this.getSGR());
|
||||
} else {
|
||||
text = strUtil.stylizeString(item.text, item.focused ? this.focusTextStyle : this.textStyle);
|
||||
sgr = (index === this.focusedItemIndex ? this.getFocusSGR() : this.getSGR());
|
||||
}
|
||||
|
||||
let renderLength = strUtil.renderStringLength(text);
|
||||
if (this.hasTextOverflow() && (item.col + renderLength) > this.dimens.width) {
|
||||
text = strUtil.renderSubstr(text, 0, this.dimens.width - (item.col + this.textOverflow.length)) + this.textOverflow;
|
||||
}
|
||||
|
||||
let padLength = Math.min(item.fixedLength + 1, this.dimens.width);
|
||||
|
||||
text = `${sgr}${strUtil.pad(text, padLength, this.fillChar, this.justify)}${this.getSGR()}`;
|
||||
this.client.term.write(`${ansi.goto(item.row, item.col)}${text}`);
|
||||
this.setRenderCacheItem(index, text, item.focused);
|
||||
};
|
||||
}
|
||||
|
||||
util.inherits(FullMenuView, MenuView);
|
||||
|
||||
FullMenuView.prototype.redraw = function() {
|
||||
FullMenuView.super_.prototype.redraw.call(this);
|
||||
|
||||
this.cachePositions();
|
||||
|
||||
if (this.items.length) {
|
||||
for (let i = this.pages[this.currentPage].start; i <= this.pages[this.currentPage].end; ++i) {
|
||||
this.items[i].focused = this.focusedItemIndex === i;
|
||||
this.drawItem(i);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
FullMenuView.prototype.setHeight = function(height) {
|
||||
this.oldDimens = Object.assign({}, this.dimens);
|
||||
|
||||
FullMenuView.super_.prototype.setHeight.call(this, height);
|
||||
|
||||
this.positionCacheExpired = true;
|
||||
this.autoAdjustHeight = false;
|
||||
};
|
||||
|
||||
FullMenuView.prototype.setWidth = function(width) {
|
||||
this.oldDimens = Object.assign({}, this.dimens);
|
||||
|
||||
FullMenuView.super_.prototype.setWidth.call(this, width);
|
||||
|
||||
this.positionCacheExpired = true;
|
||||
};
|
||||
|
||||
FullMenuView.prototype.setTextOverflow = function(overflow) {
|
||||
FullMenuView.super_.prototype.setTextOverflow.call(this, overflow);
|
||||
|
||||
this.positionCacheExpired = true;
|
||||
|
||||
}
|
||||
|
||||
FullMenuView.prototype.setPosition = function(pos) {
|
||||
FullMenuView.super_.prototype.setPosition.call(this, pos);
|
||||
|
||||
this.positionCacheExpired = true;
|
||||
};
|
||||
|
||||
FullMenuView.prototype.setFocus = function(focused) {
|
||||
FullMenuView.super_.prototype.setFocus.call(this, focused);
|
||||
this.positionCacheExpired = true;
|
||||
this.autoAdjustHeight = false;
|
||||
|
||||
this.redraw();
|
||||
};
|
||||
|
||||
FullMenuView.prototype.setFocusItemIndex = function(index) {
|
||||
FullMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex
|
||||
};
|
||||
|
||||
FullMenuView.prototype.onKeyPress = function(ch, key) {
|
||||
if (key) {
|
||||
if (this.isKeyMapped('up', key.name)) {
|
||||
this.focusPrevious();
|
||||
} else if (this.isKeyMapped('down', key.name)) {
|
||||
this.focusNext();
|
||||
} else if (this.isKeyMapped('left', key.name)) {
|
||||
this.focusPreviousColumn();
|
||||
} else if (this.isKeyMapped('right', key.name)) {
|
||||
this.focusNextColumn();
|
||||
} else if (this.isKeyMapped('page up', key.name)) {
|
||||
this.focusPreviousPageItem();
|
||||
} else if (this.isKeyMapped('page down', key.name)) {
|
||||
this.focusNextPageItem();
|
||||
} else if (this.isKeyMapped('home', key.name)) {
|
||||
this.focusFirst();
|
||||
} else if (this.isKeyMapped('end', key.name)) {
|
||||
this.focusLast();
|
||||
}
|
||||
}
|
||||
|
||||
FullMenuView.super_.prototype.onKeyPress.call(this, ch, key);
|
||||
};
|
||||
|
||||
FullMenuView.prototype.getData = function() {
|
||||
const item = this.getItem(this.focusedItemIndex);
|
||||
return _.isString(item.data) ? item.data : this.focusedItemIndex;
|
||||
};
|
||||
|
||||
FullMenuView.prototype.setItems = function(items) {
|
||||
// if we have items already, save off their drawing area so we don't leave fragments at redraw
|
||||
if (this.items && this.items.length) {
|
||||
this.oldDimens = Object.assign({}, this.dimens);
|
||||
}
|
||||
|
||||
FullMenuView.super_.prototype.setItems.call(this, items);
|
||||
|
||||
this.positionCacheExpired = true;
|
||||
};
|
||||
|
||||
FullMenuView.prototype.removeItem = function(index) {
|
||||
if (this.items && this.items.length) {
|
||||
this.oldDimens = Object.assign({}, this.dimens);
|
||||
}
|
||||
|
||||
FullMenuView.super_.prototype.removeItem.call(this, index);
|
||||
this.positionCacheExpired = true;
|
||||
};
|
||||
|
||||
FullMenuView.prototype.focusNext = function() {
|
||||
if (this.items.length - 1 === this.focusedItemIndex) {
|
||||
this.clearPage();
|
||||
this.focusedItemIndex = 0;
|
||||
this.currentPage = 0;
|
||||
}
|
||||
else {
|
||||
this.focusedItemIndex++;
|
||||
if (this.focusedItemIndex > this.pages[this.currentPage].end) {
|
||||
this.clearPage();
|
||||
this.currentPage++;
|
||||
}
|
||||
}
|
||||
|
||||
this.redraw();
|
||||
|
||||
FullMenuView.super_.prototype.focusNext.call(this);
|
||||
};
|
||||
|
||||
FullMenuView.prototype.focusPrevious = function() {
|
||||
if (0 === this.focusedItemIndex) {
|
||||
this.clearPage();
|
||||
this.focusedItemIndex = this.items.length - 1;
|
||||
this.currentPage = this.pages.length - 1;
|
||||
}
|
||||
else {
|
||||
this.focusedItemIndex--;
|
||||
if (this.focusedItemIndex < this.pages[this.currentPage].start) {
|
||||
this.clearPage();
|
||||
this.currentPage--;
|
||||
}
|
||||
}
|
||||
|
||||
this.redraw();
|
||||
|
||||
FullMenuView.super_.prototype.focusPrevious.call(this);
|
||||
};
|
||||
|
||||
FullMenuView.prototype.focusPreviousColumn = function() {
|
||||
|
||||
const currentRow = this.items[this.focusedItemIndex].itemInRow;
|
||||
this.focusedItemIndex = this.focusedItemIndex - this.itemsPerRow;
|
||||
if (this.focusedItemIndex < 0) {
|
||||
this.clearPage();
|
||||
const lastItemRow = this.items[this.items.length - 1].itemInRow;
|
||||
if (lastItemRow > currentRow) {
|
||||
this.focusedItemIndex = this.items.length - (lastItemRow - currentRow) - 1;
|
||||
}
|
||||
else {
|
||||
// can't go to same column, so go to last item
|
||||
this.focusedItemIndex = this.items.length - 1;
|
||||
}
|
||||
// set to last page
|
||||
this.currentPage = this.pages.length - 1;
|
||||
}
|
||||
else {
|
||||
if (this.focusedItemIndex < this.pages[this.currentPage].start) {
|
||||
this.clearPage();
|
||||
this.currentPage--;
|
||||
}
|
||||
}
|
||||
|
||||
this.redraw();
|
||||
|
||||
// TODO: This isn't specific to Previous, may want to replace in the future
|
||||
FullMenuView.super_.prototype.focusPrevious.call(this);
|
||||
};
|
||||
|
||||
FullMenuView.prototype.focusNextColumn = function() {
|
||||
|
||||
const currentRow = this.items[this.focusedItemIndex].itemInRow;
|
||||
this.focusedItemIndex = this.focusedItemIndex + this.itemsPerRow;
|
||||
if (this.focusedItemIndex > this.items.length - 1) {
|
||||
this.focusedItemIndex = currentRow - 1;
|
||||
this.currentPage = 0;
|
||||
this.clearPage();
|
||||
}
|
||||
else if (this.focusedItemIndex > this.pages[this.currentPage].end) {
|
||||
this.clearPage();
|
||||
this.currentPage++;
|
||||
}
|
||||
|
||||
this.redraw();
|
||||
|
||||
// TODO: This isn't specific to Next, may want to replace in the future
|
||||
FullMenuView.super_.prototype.focusNext.call(this);
|
||||
};
|
||||
|
||||
FullMenuView.prototype.focusPreviousPageItem = function() {
|
||||
|
||||
// handle first page
|
||||
if (this.currentPage == 0) {
|
||||
// Do nothing, page up shouldn't go down on last page
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentPage--;
|
||||
this.focusedItemIndex = this.pages[this.currentPage].start;
|
||||
this.clearPage();
|
||||
|
||||
this.redraw();
|
||||
|
||||
return FullMenuView.super_.prototype.focusPreviousPageItem.call(this);
|
||||
};
|
||||
|
||||
FullMenuView.prototype.focusNextPageItem = function() {
|
||||
|
||||
// handle last page
|
||||
if (this.currentPage == this.pages.length - 1) {
|
||||
// Do nothing, page up shouldn't go down on last page
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentPage++;
|
||||
this.focusedItemIndex = this.pages[this.currentPage].start;
|
||||
this.clearPage();
|
||||
|
||||
this.redraw();
|
||||
|
||||
return FullMenuView.super_.prototype.focusNextPageItem.call(this);
|
||||
};
|
||||
|
||||
FullMenuView.prototype.focusFirst = function() {
|
||||
|
||||
this.currentPage = 0;
|
||||
this.focusedItemIndex = 0;
|
||||
this.clearPage();
|
||||
|
||||
this.redraw();
|
||||
return FullMenuView.super_.prototype.focusFirst.call(this);
|
||||
};
|
||||
|
||||
FullMenuView.prototype.focusLast = function() {
|
||||
|
||||
this.currentPage = this.pages.length - 1;
|
||||
this.focusedItemIndex = this.pages[this.currentPage].end;
|
||||
this.clearPage();
|
||||
|
||||
this.redraw();
|
||||
return FullMenuView.super_.prototype.focusLast.call(this);
|
||||
};
|
||||
|
||||
FullMenuView.prototype.setFocusItems = function(items) {
|
||||
FullMenuView.super_.prototype.setFocusItems.call(this, items);
|
||||
|
||||
this.positionCacheExpired = true;
|
||||
};
|
||||
|
||||
FullMenuView.prototype.setItemSpacing = function(itemSpacing) {
|
||||
FullMenuView.super_.prototype.setItemSpacing.call(this, itemSpacing);
|
||||
|
||||
this.positionCacheExpired = true;
|
||||
};
|
||||
|
||||
FullMenuView.prototype.setJustify = function(justify) {
|
||||
FullMenuView.super_.prototype.setJustify.call(this, justify);
|
||||
this.positionCacheExpired = true;
|
||||
};
|
||||
|
||||
|
||||
FullMenuView.prototype.setItemHorizSpacing = function(itemHorizSpacing) {
|
||||
FullMenuView.super_.prototype.setItemHorizSpacing.call(this, itemHorizSpacing);
|
||||
|
||||
this.positionCacheExpired = true;
|
||||
};
|
||||
@@ -132,7 +132,7 @@ MaskEditTextView.prototype.onKeyPress = function(ch, key) {
|
||||
this.text = this.text.substr(0, this.text.length - 1);
|
||||
this.clientBackspace();
|
||||
} else {
|
||||
while(this.patternArrayPos > 0) {
|
||||
while(this.patternArrayPos >= 0) {
|
||||
if(_.isRegExp(this.patternArray[this.patternArrayPos])) {
|
||||
this.text = this.text.substr(0, this.text.length - 1);
|
||||
this.client.term.write(ansi.goto(this.position.row, this.getEndOfTextColumn() + 1));
|
||||
|
||||
@@ -8,6 +8,7 @@ const EditTextView = require('./edit_text_view.js').EditTextView;
|
||||
const ButtonView = require('./button_view.js').ButtonView;
|
||||
const VerticalMenuView = require('./vertical_menu_view.js').VerticalMenuView;
|
||||
const HorizontalMenuView = require('./horizontal_menu_view.js').HorizontalMenuView;
|
||||
const FullMenuView = require('./full_menu_view.js').FullMenuView;
|
||||
const SpinnerMenuView = require('./spinner_menu_view.js').SpinnerMenuView;
|
||||
const ToggleMenuView = require('./toggle_menu_view.js').ToggleMenuView;
|
||||
const MaskEditTextView = require('./mask_edit_text_view.js').MaskEditTextView;
|
||||
@@ -27,7 +28,7 @@ function MCIViewFactory(client) {
|
||||
}
|
||||
|
||||
MCIViewFactory.UserViewCodes = [
|
||||
'TL', 'ET', 'ME', 'MT', 'PL', 'BT', 'VM', 'HM', 'SM', 'TM', 'KE',
|
||||
'TL', 'ET', 'ME', 'MT', 'PL', 'BT', 'VM', 'HM', 'FM', 'SM', 'TM', 'KE',
|
||||
|
||||
//
|
||||
// XY is a special MCI code that allows finding positions
|
||||
@@ -164,6 +165,18 @@ MCIViewFactory.prototype.createFromMCI = function(mci) {
|
||||
view = new HorizontalMenuView(options);
|
||||
break;
|
||||
|
||||
// Full Menu
|
||||
case 'FM' :
|
||||
setOption(0, 'itemSpacing');
|
||||
setOption(1, 'itemHorizSpacing');
|
||||
setOption(2, 'justify');
|
||||
setOption(3, 'textStyle');
|
||||
|
||||
setFocusOption(0, 'focusTextStyle');
|
||||
|
||||
view = new FullMenuView(options);
|
||||
break;
|
||||
|
||||
case 'SM' :
|
||||
setOption(0, 'textStyle');
|
||||
setOption(1, 'justify');
|
||||
|
||||
@@ -56,14 +56,14 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
||||
initSequence() {
|
||||
const self = this;
|
||||
const mciData = {};
|
||||
let pausePosition;
|
||||
let pausePosition = {row: 0, column: 0};
|
||||
|
||||
const hasArt = () => {
|
||||
return _.isString(self.menuConfig.art) ||
|
||||
(Array.isArray(self.menuConfig.art) && _.has(self.menuConfig.art[0], 'acs'));
|
||||
};
|
||||
|
||||
async.series(
|
||||
async.waterfall(
|
||||
[
|
||||
function beforeArtInterrupt(callback) {
|
||||
return self.displayQueuedInterruptions(callback);
|
||||
@@ -73,7 +73,7 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
||||
},
|
||||
function displayMenuArt(callback) {
|
||||
if(!hasArt()) {
|
||||
return callback(null);
|
||||
return callback(null, null);
|
||||
}
|
||||
|
||||
self.displayAsset(
|
||||
@@ -86,18 +86,15 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
||||
mciData.menu = artData.mciMap;
|
||||
}
|
||||
|
||||
return callback(null); // any errors are non-fatal
|
||||
if(artData) {
|
||||
pausePosition.row = artData.height + 1;
|
||||
}
|
||||
|
||||
return callback(null, artData); // any errors are non-fatal
|
||||
}
|
||||
);
|
||||
},
|
||||
function moveToPromptLocation(callback) {
|
||||
if(self.menuConfig.prompt) {
|
||||
// :TODO: fetch and move cursor to prompt location, if supplied. See notes/etc. on placements
|
||||
}
|
||||
|
||||
return callback(null);
|
||||
},
|
||||
function displayPromptArt(callback) {
|
||||
function displayPromptArt(artData, callback) {
|
||||
if(!_.isString(self.menuConfig.prompt)) {
|
||||
return callback(null);
|
||||
}
|
||||
@@ -106,41 +103,41 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
||||
return callback(Errors.MissingConfig('Prompt specified but no "promptConfig" block found'));
|
||||
}
|
||||
|
||||
const options = Object.assign({}, self.menuConfig.config);
|
||||
|
||||
if(_.isNumber(artData?.height)) {
|
||||
options.startRow = artData.height + 1;
|
||||
}
|
||||
|
||||
self.displayAsset(
|
||||
self.menuConfig.promptConfig.art,
|
||||
self.menuConfig.config,
|
||||
options,
|
||||
(err, artData) => {
|
||||
if(artData) {
|
||||
mciData.prompt = artData.mciMap;
|
||||
pausePosition.row = artData.height + 1;
|
||||
}
|
||||
|
||||
return callback(err); // pass err here; prompts *must* have art
|
||||
}
|
||||
);
|
||||
},
|
||||
function recordCursorPosition(callback) {
|
||||
if(!self.shouldPause()) {
|
||||
return callback(null); // cursor position not needed
|
||||
}
|
||||
|
||||
self.client.once('cursor position report', pos => {
|
||||
pausePosition = { row : pos[0], col : 1 };
|
||||
self.client.log.trace('After art position recorded', pausePosition );
|
||||
return callback(null);
|
||||
});
|
||||
|
||||
self.client.term.rawWrite(ansi.queryPos());
|
||||
},
|
||||
function afterArtDisplayed(callback) {
|
||||
return self.mciReady(mciData, callback);
|
||||
},
|
||||
function displayPauseIfRequested(callback) {
|
||||
if(!self.shouldPause()) {
|
||||
return callback(null);
|
||||
return callback(null, null);
|
||||
}
|
||||
|
||||
if(self.client.term.termHeight > 0 && pausePosition.row > self.client.termHeight) {
|
||||
// If this scrolled, the prompt will go to the bottom of the screen
|
||||
pausePosition.row = self.client.termHeight;
|
||||
}
|
||||
|
||||
return self.pausePrompt(pausePosition, callback);
|
||||
},
|
||||
function finishAndNext(callback) {
|
||||
function finishAndNext(artInfo, callback) {
|
||||
self.finishedLoading();
|
||||
self.realTimeInterrupt = 'allowed';
|
||||
return self.autoNextMenu(callback);
|
||||
@@ -512,7 +509,7 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
||||
|
||||
this.optionalMoveToPosition(position);
|
||||
|
||||
return theme.displayThemedPause(this.client, cb);
|
||||
return theme.displayThemedPause(this.client, {position}, cb);
|
||||
}
|
||||
|
||||
promptForInput( { formName, formId, promptName, prevFormName, position } = {}, options, cb) {
|
||||
|
||||
@@ -38,14 +38,14 @@ function MenuView(options) {
|
||||
this.focusedItemIndex = options.focusedItemIndex || 0;
|
||||
this.focusedItemIndex = this.items.length >= this.focusedItemIndex ? this.focusedItemIndex : 0;
|
||||
|
||||
this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0;
|
||||
this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0;
|
||||
this.itemHorizSpacing = _.isNumber(options.itemHorizSpacing) ? options.itemHorizSpacing : 0;
|
||||
|
||||
// :TODO: probably just replace this with owner draw / pipe codes / etc. more control, less specialization
|
||||
this.focusPrefix = options.focusPrefix || '';
|
||||
this.focusSuffix = options.focusSuffix || '';
|
||||
|
||||
this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1);
|
||||
this.justify = options.justify || 'none';
|
||||
|
||||
this.hasFocusItems = function() {
|
||||
return !_.isUndefined(self.focusItems);
|
||||
@@ -68,6 +68,15 @@ function MenuView(options) {
|
||||
|
||||
util.inherits(MenuView, View);
|
||||
|
||||
MenuView.prototype.setTextOverflow = function(overflow) {
|
||||
this.textOverflow = overflow;
|
||||
this.invalidateRenderCache();
|
||||
}
|
||||
|
||||
MenuView.prototype.hasTextOverflow = function() {
|
||||
return this.textOverflow != undefined;
|
||||
}
|
||||
|
||||
MenuView.prototype.setItems = function(items) {
|
||||
if(Array.isArray(items)) {
|
||||
this.sorted = false;
|
||||
@@ -253,19 +262,32 @@ MenuView.prototype.setItemSpacing = function(itemSpacing) {
|
||||
this.positionCacheExpired = true;
|
||||
};
|
||||
|
||||
MenuView.prototype.setItemHorizSpacing = function(itemHorizSpacing) {
|
||||
itemHorizSpacing = parseInt(itemHorizSpacing);
|
||||
assert(_.isNumber(itemHorizSpacing));
|
||||
|
||||
this.itemHorizSpacing = itemHorizSpacing;
|
||||
this.positionCacheExpired = true;
|
||||
};
|
||||
|
||||
MenuView.prototype.setPropertyValue = function(propName, value) {
|
||||
switch(propName) {
|
||||
case 'itemSpacing' : this.setItemSpacing(value); break;
|
||||
case 'itemHorizSpacing' : this.setItemHorizSpacing(value); break;
|
||||
case 'items' : this.setItems(value); break;
|
||||
case 'focusItems' : this.setFocusItems(value); break;
|
||||
case 'hotKeys' : this.setHotKeys(value); break;
|
||||
case 'textOverflow' : this.setTextOverflow(value); break;
|
||||
case 'hotKeySubmit' : this.hotKeySubmit = value; break;
|
||||
case 'justify' : this.justify = value; break;
|
||||
case 'justify' : this.setJustify(value); break;
|
||||
case 'fillChar' : this.setFillChar(value); break;
|
||||
case 'focusItemIndex' : this.focusedItemIndex = value; break;
|
||||
|
||||
case 'itemFormat' :
|
||||
case 'focusItemFormat' :
|
||||
this[propName] = value;
|
||||
// if there is a cache currently, invalidate it
|
||||
this.invalidateRenderCache();
|
||||
break;
|
||||
|
||||
case 'sort' : this.setSort(value); break;
|
||||
@@ -274,6 +296,17 @@ MenuView.prototype.setPropertyValue = function(propName, value) {
|
||||
MenuView.super_.prototype.setPropertyValue.call(this, propName, value);
|
||||
};
|
||||
|
||||
MenuView.prototype.setFillChar = function(fillChar) {
|
||||
this.fillChar = miscUtil.valueWithDefault(fillChar, ' ').substr(0, 1);
|
||||
this.invalidateRenderCache();
|
||||
}
|
||||
|
||||
MenuView.prototype.setJustify = function(justify) {
|
||||
this.justify = justify;
|
||||
this.invalidateRenderCache();
|
||||
this.positionCacheExpired = true;
|
||||
}
|
||||
|
||||
MenuView.prototype.setHotKeys = function(hotKeys) {
|
||||
if(_.isObject(hotKeys)) {
|
||||
if(this.caseInsensitiveHotKeys) {
|
||||
|
||||
@@ -790,7 +790,7 @@ module.exports = class Message {
|
||||
return ftnUtil.getQuotePrefix(this[source]);
|
||||
}
|
||||
|
||||
getTearLinePosition(input) {
|
||||
static getTearLinePosition(input) {
|
||||
const m = input.match(/^--- .+$(?![\s\S]*^--- .+$)/m);
|
||||
return m ? m.index : -1;
|
||||
}
|
||||
@@ -886,12 +886,12 @@ module.exports = class Message {
|
||||
}
|
||||
);
|
||||
} else {
|
||||
const QUOTE_RE = /^ ((?:[A-Za-z0-9]{2}> )+(?:[A-Za-z0-9]{2}>)*) */;
|
||||
const QUOTE_RE = /^ ((?:[A-Za-z0-9]{1,2}> )+(?:[A-Za-z0-9]{1,2}>)*) */;
|
||||
const quoted = [];
|
||||
const input = _.trimEnd(this.message).replace(/\x08/g, ''); // eslint-disable-line no-control-regex
|
||||
|
||||
// find *last* tearline
|
||||
let tearLinePos = this.getTearLinePosition(input);
|
||||
let tearLinePos = Message.getTearLinePosition(input);
|
||||
tearLinePos = -1 === tearLinePos ? input.length : tearLinePos; // we just want the index or the entire string
|
||||
|
||||
input.slice(0, tearLinePos).split(/\r\n\r\n|\n\n/).forEach(paragraph => {
|
||||
@@ -910,7 +910,7 @@ module.exports = class Message {
|
||||
|
||||
if(quoted.length > 0) {
|
||||
//
|
||||
// Preserve paragraph seperation.
|
||||
// Preserve paragraph separation.
|
||||
//
|
||||
// FSC-0032 states something about leaving blank lines fully blank
|
||||
// (without a prefix) but it seems nicer (and more consistent with other systems)
|
||||
|
||||
@@ -426,7 +426,8 @@ exports.getModule = class mrcModule extends MenuModule {
|
||||
|
||||
switch (cmd[0]) {
|
||||
case 'pm':
|
||||
this.processOutgoingMessage(cmd[2], cmd[1]);
|
||||
const newmsg = cmd.slice(2).join(' ');
|
||||
this.processOutgoingMessage(newmsg, cmd[1]);
|
||||
break;
|
||||
|
||||
case 'rainbow': {
|
||||
|
||||
@@ -65,7 +65,7 @@ const _ = require('lodash');
|
||||
const SPECIAL_KEY_MAP_DEFAULT = {
|
||||
'line feed' : [ 'return' ],
|
||||
exit : [ 'esc' ],
|
||||
backspace : [ 'backspace' ],
|
||||
backspace : [ 'backspace', 'ctrl + d' ], // https://www.tecmint.com/linux-command-line-bash-shortcut-keys/
|
||||
delete : [ 'delete' ],
|
||||
tab : [ 'tab' ],
|
||||
up : [ 'up arrow' ],
|
||||
@@ -74,7 +74,7 @@ const SPECIAL_KEY_MAP_DEFAULT = {
|
||||
home : [ 'home' ],
|
||||
left : [ 'left arrow' ],
|
||||
right : [ 'right arrow' ],
|
||||
'delete line' : [ 'ctrl + y' ],
|
||||
'delete line' : [ 'ctrl + y', 'ctrl + u' ], // https://en.wikipedia.org/wiki/Backspace
|
||||
'page up' : [ 'page up' ],
|
||||
'page down' : [ 'page down' ],
|
||||
insert : [ 'insert', 'ctrl + v' ],
|
||||
@@ -265,11 +265,10 @@ function MultiLineEditTextView(options) {
|
||||
|
||||
this.getRenderText = function(index) {
|
||||
let text = self.getVisibleText(index);
|
||||
const remain = self.dimens.width - text.length;
|
||||
const remain = self.dimens.width - strUtil.renderStringLength(text);
|
||||
|
||||
if(remain > 0) {
|
||||
text += ' '.repeat(remain + 1);
|
||||
// text += new Array(remain + 1).join(' ');
|
||||
text += ' '.repeat(remain);// + 1);
|
||||
}
|
||||
|
||||
return text;
|
||||
|
||||
@@ -135,7 +135,7 @@ exports.getModule = class OnelinerzModule extends MenuModule {
|
||||
self.db.each(
|
||||
`SELECT *
|
||||
FROM (
|
||||
SELECT *
|
||||
SELECT *
|
||||
FROM onelinerz
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT ${limit}
|
||||
@@ -248,8 +248,9 @@ exports.getModule = class OnelinerzModule extends MenuModule {
|
||||
async.series(
|
||||
[
|
||||
function openDatabase(callback) {
|
||||
const dbSuffix = self.menuConfig.config.dbSuffix;
|
||||
self.db = getTransactionDatabase(new sqlite3.Database(
|
||||
getModDatabasePath(exports.moduleInfo),
|
||||
getModDatabasePath(exports.moduleInfo, dbSuffix),
|
||||
err => {
|
||||
return callback(err);
|
||||
}
|
||||
@@ -260,7 +261,7 @@ exports.getModule = class OnelinerzModule extends MenuModule {
|
||||
`CREATE TABLE IF NOT EXISTS onelinerz (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER_NOT NULL,
|
||||
user_name VARCHAR NOT NULL,
|
||||
user_name VARCHAR NOT NULL,
|
||||
oneliner VARCHAR NOT NULL,
|
||||
timestamp DATETIME NOT NULL
|
||||
);`
|
||||
|
||||
@@ -153,6 +153,8 @@ function scanFileAreaForChanges(areaInfo, options, cb) {
|
||||
function updateTags(fe) {
|
||||
if(Array.isArray(options.tags)) {
|
||||
fe.hashTags = new Set(options.tags);
|
||||
} else if (areaInfo.hashTags) { // no explicit tags; merge in defaults, if any
|
||||
fe.hashTags = areaInfo.hashTags;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,7 +229,8 @@ function scanFileAreaForChanges(areaInfo, options, cb) {
|
||||
fullPath,
|
||||
{
|
||||
areaTag : areaInfo.areaTag,
|
||||
storageTag : storageLoc.storageTag
|
||||
storageTag : storageLoc.storageTag,
|
||||
hashTags : areaInfo.hashTags,
|
||||
},
|
||||
(stepInfo, next) => {
|
||||
if(argv.verbose) {
|
||||
@@ -549,7 +552,7 @@ function scanFileAreas() {
|
||||
console.info(`Processing area "${areaInfo.name}":`);
|
||||
|
||||
scanFileAreaForChanges(areaInfo, options, err => {
|
||||
return callback(err);
|
||||
return nextAreaTag(err);
|
||||
});
|
||||
}, err => {
|
||||
return callback(err);
|
||||
|
||||
@@ -57,7 +57,7 @@ Actions:
|
||||
|
||||
lock USERNAME Set a user's status to "locked"
|
||||
|
||||
group USERNAME [+|-]GROUP Adds (+) or removes (-) user from a group
|
||||
group USERNAME [+|~]GROUP Adds (+) or removes (~) user from a group
|
||||
|
||||
list [FILTER] List users with optional FILTER.
|
||||
|
||||
|
||||
@@ -172,6 +172,7 @@ function removeUser(user) {
|
||||
message : [ 'user_message_area_last_read' ],
|
||||
system : [ 'user_event_log', ],
|
||||
user : [ 'user_group_member', 'user' ],
|
||||
file : [ 'file_user_rating']
|
||||
};
|
||||
|
||||
async.eachSeries(Object.keys(DeleteFrom), (dbName, nextDbName) => {
|
||||
@@ -275,7 +276,7 @@ function modUserGroups(user) {
|
||||
let groupName = argv._[argv._.length - 1].toString().replace(/["']/g, ''); // remove any quotes - necessary to allow "-foo"
|
||||
let action = groupName[0]; // + or -
|
||||
|
||||
if('-' === action || '+' === action) {
|
||||
if('-' === action || '+' === action || '~' === action) {
|
||||
groupName = groupName.substr(1);
|
||||
}
|
||||
|
||||
@@ -286,7 +287,7 @@ function modUserGroups(user) {
|
||||
}
|
||||
|
||||
//
|
||||
// Groups are currently arbritary, so do a slight validation
|
||||
// Groups are currently arbitrary, so do a slight validation
|
||||
//
|
||||
if(!/[A-Za-z0-9]+/.test(groupName)) {
|
||||
process.exitCode = ExitCodes.BAD_ARGS;
|
||||
@@ -303,7 +304,7 @@ function modUserGroups(user) {
|
||||
}
|
||||
|
||||
const UserGroup = require('../../core/user_group.js');
|
||||
if('-' === action) {
|
||||
if('-' === action || '~' === action) {
|
||||
UserGroup.removeUserFromGroup(user.userId, groupName, done);
|
||||
} else {
|
||||
UserGroup.addUserToGroup(user.userId, groupName, done);
|
||||
|
||||
@@ -941,7 +941,7 @@ class QWKPacketWriter extends EventEmitter {
|
||||
}
|
||||
|
||||
// First block is a space padded ID
|
||||
const id = `Created with ENiGMA 1/2 BBS v${enigmaVersion} Copyright (c) 2015-2020 Bryan Ashby`;
|
||||
const id = `Created with ENiGMA 1/2 BBS v${enigmaVersion} Copyright (c) 2015-2022 Bryan Ashby`;
|
||||
this.messagesStream.write(id.padEnd(QWKMessageBlockSize, ' '), 'ascii');
|
||||
this.currentMessageOffset = QWKMessageBlockSize;
|
||||
|
||||
|
||||
@@ -81,14 +81,22 @@ exports.getModule = class GopherModule extends ServerModule {
|
||||
this.publicHostname = config.contentServers.gopher.publicHostname;
|
||||
this.publicPort = config.contentServers.gopher.publicPort;
|
||||
|
||||
this.addRoute(/^\/?\r\n$/, this.defaultGenerator);
|
||||
this.addRoute(/^\/msgarea(\/[a-z0-9_-]+(\/[a-z0-9_-]+)?(\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(_raw)?)?)?\/?\r\n$/, this.messageAreaGenerator);
|
||||
this.addRoute(/^\/?msgarea(\/[a-z0-9_-]+(\/[a-z0-9_-]+)?(\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(_raw)?)?)?\/?\r\n$/, this.messageAreaGenerator);
|
||||
this.addRoute(/^(\/?[^\t\r\n]*)\r\n$/, this.staticGenerator);
|
||||
|
||||
this.server = net.createServer( socket => {
|
||||
socket.setEncoding('ascii');
|
||||
|
||||
socket.on('data', data => {
|
||||
this.routeRequest(data, socket);
|
||||
// sanitize a bit - bots like to inject garbage
|
||||
data = data.replace(/[^ -~\t\r\n]/g, '');
|
||||
if (data) {
|
||||
this.routeRequest(data, socket);
|
||||
} else {
|
||||
this.notFoundGenerator('**invalid selector**', res => {
|
||||
return socket.end(`${res}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', err => {
|
||||
@@ -161,22 +169,56 @@ exports.getModule = class GopherModule extends ServerModule {
|
||||
return `${itemType}${text}\t${selector}\t${hostname}\t${port}\r\n`;
|
||||
}
|
||||
|
||||
defaultGenerator(selectorMatch, cb) {
|
||||
this.log.debug( { selector : selectorMatch[0] }, 'Serving default content');
|
||||
staticGenerator(selectorMatch, cb) {
|
||||
this.log.debug( { selector : selectorMatch[1] || '(gophermap)' }, 'Serving static content');
|
||||
|
||||
let bannerFile = _.get(Config(), 'contentServers.gopher.bannerFile', 'gopher_banner.asc');
|
||||
bannerFile = paths.isAbsolute(bannerFile) ? bannerFile : paths.join(__dirname, '../../../misc', bannerFile);
|
||||
fs.readFile(bannerFile, 'utf8', (err, banner) => {
|
||||
if(err) {
|
||||
return cb('You have reached an ENiGMA½ Gopher server!');
|
||||
const requestedPath = selectorMatch[1];
|
||||
let path = this.resolveContentPath(requestedPath);
|
||||
if (!path) {
|
||||
return cb('Not found');
|
||||
}
|
||||
|
||||
fs.stat(path, (err, stats) => {
|
||||
if (err) {
|
||||
return cb('Not found');
|
||||
}
|
||||
|
||||
banner = splitTextAtTerms(banner).map(l => this.makeItem(ItemTypes.InfoMessage, l)).join('');
|
||||
banner += this.makeItem(ItemTypes.SubMenu, 'Public Message Area', '/msgarea');
|
||||
return cb(banner);
|
||||
let isGopherMap = false;
|
||||
if (stats.isDirectory()) {
|
||||
path = paths.join(path, 'gophermap');
|
||||
isGopherMap = true;
|
||||
}
|
||||
|
||||
fs.readFile(path, isGopherMap ? 'utf8' : null, (err, content) => {
|
||||
if (err) {
|
||||
let content = 'You have reached an ENiGMA½ Gopher server!\r\n';
|
||||
content += this.makeItem(ItemTypes.SubMenu, 'Public Message Area', '/msgarea');
|
||||
return cb(content);
|
||||
}
|
||||
|
||||
if (isGopherMap) {
|
||||
// Convert any UNIX style LF's to DOS CRLF's
|
||||
content = content.replace(/\r?\n/g, '\r\n');
|
||||
|
||||
// variable support
|
||||
content = content
|
||||
.replace(/{publicHostname}/g, this.publicHostname)
|
||||
.replace(/{publicPort}/g, this.publicPort);
|
||||
}
|
||||
|
||||
return cb(content);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
resolveContentPath(requestPath) {
|
||||
const staticRoot = _.get(Config(), 'contentServers.gopher.staticRoot');
|
||||
const path = paths.resolve(staticRoot, `.${requestPath}`);
|
||||
if (path.startsWith(staticRoot)) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
notFoundGenerator(selector, cb) {
|
||||
this.log.debug( { selector }, 'Serving not found content');
|
||||
return cb('Not found');
|
||||
|
||||
@@ -125,7 +125,7 @@ class NNTPServer extends NNTPServerBase {
|
||||
const config = Config();
|
||||
this.groupCache = new LRU({
|
||||
max : _.get(config, 'contentServers.nntp.cache.maxItems', 200),
|
||||
maxAge : _.get(config, 'contentServers.nntp.cache.maxAge', 1000 * 30), // default=30s
|
||||
ttl : _.get(config, 'contentServers.nntp.cache.maxAge', 1000 * 30), // default=30s
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -215,20 +215,22 @@ exports.getModule = class WebServerModule extends ServerModule {
|
||||
|
||||
routeIndex(req, resp) {
|
||||
const filePath = paths.join(Config().contentServers.web.staticRoot, 'index.html');
|
||||
|
||||
return this.returnStaticPage(filePath, resp);
|
||||
}
|
||||
|
||||
routeStaticFile(req, resp) {
|
||||
const fileName = req.url.substr(req.url.indexOf('/', 1));
|
||||
const filePath = paths.join(Config().contentServers.web.staticRoot, fileName);
|
||||
|
||||
const filePath = this.resolveStaticPath(fileName);
|
||||
return this.returnStaticPage(filePath, resp);
|
||||
}
|
||||
|
||||
returnStaticPage(filePath, resp) {
|
||||
const self = this;
|
||||
|
||||
if (!filePath) {
|
||||
return this.fileNotFound(resp);
|
||||
}
|
||||
|
||||
fs.stat(filePath, (err, stats) => {
|
||||
if(err || !stats.isFile()) {
|
||||
return self.fileNotFound(resp);
|
||||
@@ -245,6 +247,14 @@ exports.getModule = class WebServerModule extends ServerModule {
|
||||
});
|
||||
}
|
||||
|
||||
resolveStaticPath(requestPath) {
|
||||
const staticRoot = _.get(Config(), 'contentServers.web.staticRoot');
|
||||
const path = paths.resolve(staticRoot, `.${requestPath}`);
|
||||
if (path.startsWith(staticRoot)) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
routeTemplateFilePage(templatePath, preprocessCallback, resp) {
|
||||
const self = this;
|
||||
|
||||
|
||||
@@ -357,9 +357,11 @@ exports.getModule = class SSHServerModule extends LoginServerModule {
|
||||
// However, as of this writing, NetRunner and SyncTERM both
|
||||
// fail to respond to OpenSSH keep-alive pings (keepalive@openssh.com)
|
||||
//
|
||||
ssh2.Server.KEEPALIVE_INTERVAL = 0;
|
||||
// See also #399
|
||||
//
|
||||
ssh2.Server.KEEPALIVE_CLIENT_INTERVAL = 0;
|
||||
|
||||
this.server = ssh2.Server(serverConf);
|
||||
this.server = new ssh2.Server(serverConf);
|
||||
this.server.on('connection', (conn, info) => {
|
||||
Log.info(info, 'New SSH connection');
|
||||
this.handleNewClient(new SSHClient(conn), conn._sock, ModuleInfo);
|
||||
|
||||
@@ -103,14 +103,16 @@ class TelnetClient {
|
||||
case Options.NEW_ENVIRON :
|
||||
{
|
||||
this._logDebug(
|
||||
{ vars : command.optionData.vars, userVars : command.optionData.userVars },
|
||||
{ vars : command.optionData.vars, uservars : command.optionData.uservars },
|
||||
'New environment received'
|
||||
);
|
||||
|
||||
// get a value from vars with fallback of user vars
|
||||
const getValue = (name) => {
|
||||
return command.optionData.vars.find(nv => nv.name === name) ||
|
||||
command.optionData.userVars.find(nv => nv.name === name);
|
||||
return command.optionData.vars &&
|
||||
(command.optionData.vars.find(nv => nv.name === name) ||
|
||||
command.optionData.uservars.find(nv => nv.name === name)
|
||||
);
|
||||
};
|
||||
|
||||
if ('unknown' === this.term.termType) {
|
||||
|
||||
@@ -171,22 +171,15 @@ exports.getModule = class ShowArtModule extends MenuModule {
|
||||
return callback(err);
|
||||
}
|
||||
const mciData = { menu : artData.mciMap };
|
||||
return callback(null, mciData);
|
||||
if(self.client.term.termHeight > 0 && artData.height > self.client.term.termHeight) {
|
||||
// We must have scrolled, adjust the positioning for pause
|
||||
artData.height = self.client.term.termHeight;
|
||||
}
|
||||
const pausePosition = { row: artData.height + 1, col: 1};
|
||||
return callback(null, mciData, pausePosition);
|
||||
}
|
||||
);
|
||||
},
|
||||
function recordCursorPosition(mciData, callback) {
|
||||
if(!options.pause) {
|
||||
return callback(null, mciData, null); // cursor position not needed
|
||||
}
|
||||
|
||||
self.client.once('cursor position report', pos => {
|
||||
const pausePosition = { row : pos[0], col : 1 };
|
||||
return callback(null, mciData, pausePosition);
|
||||
});
|
||||
|
||||
self.client.term.rawWrite(ANSI.queryPos());
|
||||
},
|
||||
function afterArtDisplayed(mciData, pausePosition, callback) {
|
||||
self.mciReady(mciData, err => {
|
||||
return callback(err, pausePosition);
|
||||
|
||||
@@ -20,6 +20,7 @@ exports.stringFromNullTermBuffer = stringFromNullTermBuffer;
|
||||
exports.stringToNullTermBuffer = stringToNullTermBuffer;
|
||||
exports.renderSubstr = renderSubstr;
|
||||
exports.renderStringLength = renderStringLength;
|
||||
exports.ansiRenderStringLength = ansiRenderStringLength;
|
||||
exports.formatByteSizeAbbr = formatByteSizeAbbr;
|
||||
exports.formatByteSize = formatByteSize;
|
||||
exports.formatCountAbbr = formatCountAbbr;
|
||||
@@ -297,7 +298,7 @@ function renderStringLength(s) {
|
||||
let len = 0;
|
||||
|
||||
const re = ANSI_OR_PIPE_REGEXP;
|
||||
re.lastIndex = 0; // we recycle the rege; reset
|
||||
re.lastIndex = 0; // we recycle the regex; reset
|
||||
|
||||
//
|
||||
// Loop counting only literal (non-control) sequences
|
||||
@@ -312,7 +313,41 @@ function renderStringLength(s) {
|
||||
len += s.slice(pos, m.index).length;
|
||||
}
|
||||
|
||||
if('C' === m[3]) { // ESC[<N>C is foward/right
|
||||
if('C' === m[3]) { // ESC[<N>C is forward/right
|
||||
len += parseInt(m[2], 10) || 0;
|
||||
}
|
||||
}
|
||||
} while(0 !== re.lastIndex);
|
||||
|
||||
if(pos < s.length) {
|
||||
len += s.slice(pos).length;
|
||||
}
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
// Like renderStringLength() but ANSI only (no pipe codes accounted for)
|
||||
function ansiRenderStringLength(s) {
|
||||
let m;
|
||||
let pos;
|
||||
let len = 0;
|
||||
|
||||
const re = ANSI.getFullMatchRegExp();
|
||||
|
||||
//
|
||||
// Loop counting only literal (non-control) sequences
|
||||
// paying special attention to ESC[<N>C which means forward <N>
|
||||
//
|
||||
do {
|
||||
pos = re.lastIndex;
|
||||
m = re.exec(s);
|
||||
|
||||
if(m) {
|
||||
if(m.index > pos) {
|
||||
len += s.slice(pos, m.index).length;
|
||||
}
|
||||
|
||||
if('C' === m[3]) { // ESC[<N>C is forward/right
|
||||
len += parseInt(m[2], 10) || 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ exports.nextConf = nextConf;
|
||||
exports.prevArea = prevArea;
|
||||
exports.nextArea = nextArea;
|
||||
exports.sendForgotPasswordEmail = sendForgotPasswordEmail;
|
||||
exports.optimizeDatabases = optimizeDatabases;
|
||||
|
||||
const handleAuthFailures = (callingMenu, err, cb) => {
|
||||
// already logged in with this user?
|
||||
@@ -205,3 +206,25 @@ function sendForgotPasswordEmail(callingMenu, formData, extraArgs, cb) {
|
||||
return logoff(callingMenu, formData, extraArgs, cb);
|
||||
});
|
||||
}
|
||||
|
||||
function optimizeDatabases(callingMenu, formData, extraArgs, cb) {
|
||||
const dbs = require('./database').dbs;
|
||||
const client = callingMenu.client;
|
||||
|
||||
client.term.write('\r\n\r\n');
|
||||
|
||||
Object.keys(dbs).forEach(dbName => {
|
||||
client.log.info({ dbName }, 'Optimizing database');
|
||||
|
||||
client.term.write(`Optimizing ${dbName}. Please wait...\r\n`);
|
||||
|
||||
// https://www.sqlite.org/pragma.html#pragma_optimize
|
||||
dbs[dbName].run('PRAGMA optimize;', err => {
|
||||
if (err) {
|
||||
client.log.error({ error : err, dbName }, 'Error attempting to optimize database');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return callingMenu.prevMenu(cb);
|
||||
}
|
||||
@@ -44,49 +44,6 @@ function TextView(options) {
|
||||
this.textMaskChar = options.textMaskChar;
|
||||
}
|
||||
|
||||
/*
|
||||
this.drawText = function(s) {
|
||||
|
||||
//
|
||||
// |<- this.maxLength
|
||||
// ABCDEFGHIJK
|
||||
// |ABCDEFG| ^_ this.text.length
|
||||
// ^-- this.dimens.width
|
||||
//
|
||||
let textToDraw = _.isString(this.textMaskChar) ?
|
||||
new Array(s.length + 1).join(this.textMaskChar) :
|
||||
stylizeString(s, this.hasFocus ? this.focusTextStyle : this.textStyle);
|
||||
|
||||
if(textToDraw.length > this.dimens.width) {
|
||||
if(this.hasFocus) {
|
||||
if(this.horizScroll) {
|
||||
textToDraw = textToDraw.substr(textToDraw.length - this.dimens.width, textToDraw.length);
|
||||
}
|
||||
} else {
|
||||
if(textToDraw.length > this.dimens.width) {
|
||||
if(this.textOverflow &&
|
||||
this.dimens.width > this.textOverflow.length &&
|
||||
textToDraw.length - this.textOverflow.length >= this.textOverflow.length)
|
||||
{
|
||||
textToDraw = textToDraw.substr(0, this.dimens.width - this.textOverflow.length) + this.textOverflow;
|
||||
} else {
|
||||
textToDraw = textToDraw.substr(0, this.dimens.width);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.client.term.write(padStr(
|
||||
textToDraw,
|
||||
this.dimens.width + 1,
|
||||
this.fillChar,
|
||||
this.justify,
|
||||
this.hasFocus ? this.getFocusSGR() : this.getSGR(),
|
||||
this.getStyleSGR(1) || this.getSGR()
|
||||
), false);
|
||||
};
|
||||
*/
|
||||
|
||||
this.drawText = function(s) {
|
||||
|
||||
//
|
||||
@@ -125,7 +82,7 @@ function TextView(options) {
|
||||
this.client.term.write(
|
||||
padStr(
|
||||
textToDraw,
|
||||
this.dimens.width + 1,
|
||||
this.dimens.width,
|
||||
renderedFillChar, //this.fillChar,
|
||||
this.justify,
|
||||
this.hasFocus ? this.getFocusSGR() : this.getSGR(),
|
||||
|
||||
@@ -495,6 +495,7 @@ function displayPreparedArt(options, artInfo, cb) {
|
||||
sauce : artInfo.sauce,
|
||||
font : options.font,
|
||||
trailingLF : options.trailingLF,
|
||||
startRow : options.startRow,
|
||||
};
|
||||
art.display(options.client, artInfo.data, displayOpts, (err, mciMap, extraInfo) => {
|
||||
return cb(err, { mciMap : mciMap, artInfo : artInfo, extraInfo : extraInfo } );
|
||||
@@ -551,6 +552,7 @@ function displayThemedPrompt(name, client, options, cb) {
|
||||
|
||||
if(options.clearScreen) {
|
||||
client.term.rawWrite(ansi.resetScreen());
|
||||
options.position = {row: 1, column: 1};
|
||||
}
|
||||
|
||||
//
|
||||
@@ -560,9 +562,9 @@ function displayThemedPrompt(name, client, options, cb) {
|
||||
//
|
||||
const dispOptions = Object.assign( {}, options, promptConfig.config );
|
||||
// :TODO: We can use term detection to do nifty things like avoid this kind of kludge:
|
||||
if(!options.clearScreen) {
|
||||
dispOptions.font = 'not_really_a_font!'; // kludge :)
|
||||
}
|
||||
// if(!options.clearScreen) {
|
||||
// dispOptions.font = 'not_really_a_font!'; // kludge :)
|
||||
// }
|
||||
|
||||
displayThemedAsset(
|
||||
promptConfig.art,
|
||||
@@ -583,12 +585,15 @@ function displayThemedPrompt(name, client, options, cb) {
|
||||
return callback(null, promptConfig, artInfo);
|
||||
}
|
||||
|
||||
client.once('cursor position report', pos => {
|
||||
artInfo.startRow = pos[0] - artInfo.height;
|
||||
return callback(null, promptConfig, artInfo);
|
||||
});
|
||||
if(_.isNumber(options?.position?.row)) {
|
||||
artInfo.startRow = options.position.row;
|
||||
if(client.term.termHeight > 0 && artInfo.startRow + artInfo.height > client.term.termHeight) {
|
||||
// in this case, we will have scrolled
|
||||
artInfo.startRow = client.term.termHeight - artInfo.height;
|
||||
}
|
||||
}
|
||||
|
||||
client.term.rawWrite(ansi.queryPos());
|
||||
return callback(null, promptConfig, artInfo);
|
||||
},
|
||||
function createMCIViews(promptConfig, artInfo, callback) {
|
||||
const assocViewController = usingTempViewController ? new ViewController( { client : client } ) : options.viewController;
|
||||
@@ -614,7 +619,9 @@ function displayThemedPrompt(name, client, options, cb) {
|
||||
});
|
||||
},
|
||||
function clearPauseArt(artInfo, assocViewController, callback) {
|
||||
if(options.clearPrompt) {
|
||||
// Only clear with height if clearPrompt is true and if we were able
|
||||
// to determine the row
|
||||
if(options.clearPrompt && artInfo.startRow) {
|
||||
if(artInfo.startRow && artInfo.height) {
|
||||
client.term.rawWrite(ansi.goto(artInfo.startRow, 1));
|
||||
|
||||
|
||||
@@ -332,6 +332,7 @@ exports.getModule = class UploadModule extends MenuModule {
|
||||
const scanOpts = {
|
||||
areaTag : self.areaInfo.areaTag,
|
||||
storageTag : self.areaInfo.storageTags[0],
|
||||
hashTags : self.areaInfo.hashTags,
|
||||
};
|
||||
|
||||
function handleScanStep(stepInfo, nextScanStep) {
|
||||
|
||||
18
core/view.js
18
core/view.js
@@ -17,7 +17,7 @@ exports.View = View;
|
||||
const VIEW_SPECIAL_KEY_MAP_DEFAULT = {
|
||||
accept : [ 'return' ],
|
||||
exit : [ 'esc' ],
|
||||
backspace : [ 'backspace', 'del' ],
|
||||
backspace : [ 'backspace', 'del', 'ctrl + d'], // https://www.tecmint.com/linux-command-line-bash-shortcut-keys/
|
||||
del : [ 'del' ],
|
||||
next : [ 'tab' ],
|
||||
up : [ 'up arrow' ],
|
||||
@@ -154,7 +154,7 @@ View.prototype.setHeight = function(height) {
|
||||
|
||||
View.prototype.setWidth = function(width) {
|
||||
width = parseInt(width) || 1;
|
||||
width = Math.min(width, this.client.term.termWidth);
|
||||
width = Math.min(width, this.client.term.termWidth - this.position.col);
|
||||
|
||||
this.dimens.width = width;
|
||||
};
|
||||
@@ -186,7 +186,7 @@ View.prototype.setPropertyValue = function(propName, value) {
|
||||
|
||||
case 'height' : this.setHeight(value); break;
|
||||
case 'width' : this.setWidth(value); break;
|
||||
case 'focus' : this.setFocus(value); break;
|
||||
case 'focus' : this.setFocusProperty(value); break;
|
||||
|
||||
case 'text' :
|
||||
if('setText' in this) {
|
||||
@@ -252,10 +252,16 @@ View.prototype.redraw = function() {
|
||||
this.client.term.write(ansi.goto(this.position.row, this.position.col));
|
||||
};
|
||||
|
||||
View.prototype.setFocus = function(focused) {
|
||||
enigAssert(this.acceptsFocus, 'View does not accept focus');
|
||||
|
||||
View.prototype.setFocusProperty = function(focused) {
|
||||
// Either this should accept focus, or the focus should be false
|
||||
enigAssert(this.acceptsFocus || !focused, 'View does not accept focus');
|
||||
this.hasFocus = focused;
|
||||
};
|
||||
|
||||
View.prototype.setFocus = function(focused) {
|
||||
// Call separate method to differentiate between a value set as a
|
||||
// property vs focus programmatically called.
|
||||
this.setFocusProperty(focused);
|
||||
this.restoreCursor();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
const renderStringLength = require('./string_util.js').renderStringLength;
|
||||
const {
|
||||
ansiRenderStringLength,
|
||||
} = require('./string_util');
|
||||
|
||||
// deps
|
||||
const assert = require('assert');
|
||||
@@ -28,7 +30,7 @@ function wordWrapText(text, options) {
|
||||
|
||||
//const REGEXP_GOBBLE = new RegExp(`.{0,${options.width}}`, 'g');
|
||||
//
|
||||
// For a given word, match 0->options.width chars -- alwasy include a full trailing ESC
|
||||
// For a given word, match 0->options.width chars -- always include a full trailing ESC
|
||||
// sequence if present!
|
||||
//
|
||||
// :TODO: Need to create ansi.getMatchRegex or something - this is used all over
|
||||
@@ -49,7 +51,7 @@ function wordWrapText(text, options) {
|
||||
|
||||
function appendWord() {
|
||||
word.match(REGEXP_GOBBLE).forEach( w => {
|
||||
renderLen = renderStringLength(w);
|
||||
renderLen = ansiRenderStringLength(w);
|
||||
|
||||
if(result.renderLen[i] + renderLen > options.width) {
|
||||
if(0 === i) {
|
||||
@@ -70,7 +72,7 @@ function wordWrapText(text, options) {
|
||||
//
|
||||
// * Sublime Text 3 for example considers spaces after a word
|
||||
// part of said word. For example, "word " would be wraped
|
||||
// in it's entirity.
|
||||
// in it's entirety.
|
||||
//
|
||||
// * Tabs in Sublime Text 3 are also treated as a word, so, e.g.
|
||||
// "\t" may resolve to " " and must fit within the space.
|
||||
|
||||
Reference in New Issue
Block a user