Merge branch '216-waiting-for-caller' of github.com:NuSkooler/enigma-bbs into 216-waiting-for-caller

This commit is contained in:
Bryan Ashby 2022-04-08 17:39:48 -06:00
commit 5288f82006
No known key found for this signature in database
GPG Key ID: B49EB437951D2542
162 changed files with 3335 additions and 1792 deletions

View File

@ -28,5 +28,8 @@
],
"comma-dangle": 0,
"no-trailing-spaces" :"warn"
},
"parserOptions": {
"ecmaVersion": 2020
}
}

35
.github/workflows/docker.yml vendored Normal file
View File

@ -0,0 +1,35 @@
name: Docker
on:
push:
branches: [ master ]
workflow_dispatch:
jobs:
buildx:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v2
with:
tags: enigmabbs/enigma-bbs:latest
file: docker/Dockerfile
platforms: linux/amd64,linux/arm64
push: true

75
.github/workflows/jekyll.yml vendored Normal file
View File

@ -0,0 +1,75 @@
name: Build and deploy jekyll site
on:
push:
branches:
- master
# - source
# It is highly recommended that you only run this action on push to a
# specific branch, eg. master or source (if on *.github.io repo)
jobs:
jekyll:
runs-on: ubuntu-latest # can change this to ubuntu-latest if you prefer
steps:
- name: 📂 setup
uses: actions/checkout@v2
# include the lines below if you are using jekyll-last-modified-at
# or if you would otherwise need to fetch the full commit history
# however this may be very slow for large repositories!
# with:
# fetch-depth: '0'
- name: 💎 setup ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 2.7 # can change this to 2.7 or whatever version you prefer
- name: 🔨 install dependencies & build site
uses: limjh16/jekyll-action-ts@v2
with:
enable_cache: true
### Enables caching. Similar to https://github.com/actions/cache.
#
# format_output: true
### Uses prettier https://prettier.io to format jekyll output HTML.
#
# prettier_opts: '{ "useTabs": true }'
### Sets prettier options (in JSON) to format output HTML. For example, output tabs over spaces.
### Possible options are outlined in https://prettier.io/docs/en/options.html
#
# prettier_ignore: 'about/*'
### Ignore paths for prettier to not format those html files.
### Useful if the file is exceptionally large, so formatting it takes a while.
### Also useful if HTML compression is enabled for that file / formatting messes it up.
#
jekyll_src: docs
### If the jekyll website source is not in root, specify the directory. (in this case, sample_site)
### By default, this is not required as the action searches for a _config.yml automatically.
#
gem_src: docs
### By default, this is not required as the action searches for a _config.yml automatically.
### However, if there are multiple Gemfiles, the action may not be able to determine which to use.
### In that case, specify the directory. (in this case, sample_site)
###
### If jekyll_src is set, the action would automatically choose the Gemfile in jekyll_src.
### In that case this input may not be needed as well.
#
# key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
# restore-keys: ${{ runner.os }}-gems-
### In cases where you want to specify the cache key, enable the above 2 inputs
### Follows the format here https://github.com/actions/cache
#
# custom_opts: '--drafts --lsi'
### If you need to specify any Jekyll build options, enable the above input
### Flags accepted can be found here https://jekyllrb.com/docs/configuration/options/#build-command-options
- name: 🚀 deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./_site
# if the repo you are deploying to is <username>.github.io, uncomment the line below.
# if you are including the line below, make sure your source files are NOT in the master branch:
# publish_branch: master

6
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,6 @@
# Contributing
## Style
Though you'll see a lot of older style callback code, please utilize modern JavaScript. ES6 classes, arrow functions, and builtins.
There is almost never a reason to use `var`. Prefer `const` where you can and and `let` otherwise.
Save with UNIX line feeds, UTF-8 without BOM, and tabs set to 4 spaces.

View File

@ -1,4 +1,4 @@
Copyright (c) 2015-2020, Bryan D. Ashby
Copyright (c) 2015-2022, Bryan D. Ashby
All rights reserved.
Redistribution and use in source and binary forms, with or without

View File

@ -46,7 +46,7 @@ If you feel the urge to donate, [you can do so here](https://liberapay.com/NuSko
## Support
* See [Discussions](https://github.com/NuSkooler/enigma-bbs/discussions) and [the issue tracker](https://github.com/NuSkooler/enigma-bbs/issues)
* **Discussion on a ENiGMA BBS!** (see Boards below)
* IRC: **#enigma-bbs** on **chat.freenode.net** ([webchat](https://webchat.freenode.net/?channels=enigma-bbs))
* IRC: **#enigma-bbs** on **irc.libera.chat:6697(TLS)** ([webchat](https://web.libera.chat/gamja/?channels=#enigma-bbs))
* FSX_ENG on [fsxNet](http://bbs.geek.nz/#fsxNet) or ARK_ENIG on [ArakNet](https://www.araknet.xyz/) available on many fine boards
* Email: bryan -at- l33t.codes
* [Facebook ENiGMA½ group](https://www.facebook.com/groups/enigmabbs/)
@ -64,9 +64,9 @@ ENiGMA has been tested with many terminals. However, the following are suggested
* [fORCE9](http://bbs.force9.org/): (**telnet://bbs.force9.org**)
* [Undercurrents](https://undercurrents.io): (**ssh://undercurrents.io**)
* [PlaneT Afr0](https://planetafr0.org/): (**ssh://planetafr0.org:8889**)
* [Goblin Studio](https://goblin.strangled.net): (**ssh://goblin.strangled.net:8889**)
## Special Thanks
(in no particular order)
* [Dave Stephens aka RiPuk](https://github.com/davestephens) for the awesome [ENiGMA website](https://enigma-bbs.github.io/) and [KICK ASS documentation](https://nuskooler.github.io/enigma-bbs/), code contributions, etc.
* [Daniel Mecklenburg Jr.](https://github.com/codewar65) for the awesome VTX terminal and general coding talk
* [M. Brutman](http://www.brutman.com/), author of [mTCP](http://www.brutman.com/mTCP/mTCP.html) (Interwebs for DOS!)
@ -81,15 +81,16 @@ ENiGMA has been tested with many terminals. However, the following are suggested
* [nail/blocktronics](http://blocktronics.org/tag/nail/) for the [sickmade Xibalba logo](http://pc.textmod.es/pack/blocktronics-420/n-xbalba.ans)!
* [Whazzit/blocktronics](http://blocktronics.org/tag/whazzit/) for the amazing Mayan ANSI pieces scattered about Xibalba BBS!
* [Smooth](https://16colo.rs/tags/artist/smooth)/[fUEL](https://fuel.wtf/) for lots of dope art. Why not [snag a T-Shirt](https://www.redbubble.com/people/araknet/works/39126831-enigma-1-2-software-logo-design-by-smooth-of-fuel?p=t-shirt)?
* Al's Geek Lab for the [installation video](https://youtu.be/WnN-ucVi3ZU)!
* Al's Geek Lab for the [installation video](https://youtu.be/WnN-ucVi3ZU) and of course the [Back to the BBS - Part one: The return to being online](https://www.youtube.com/watch?reload=9&v=n0OwGSX2IiQ) documentary!
* Alpha for the [FTN-style configuration guide](https://medium.com/@alpha_11845/setting-up-ftn-style-message-networks-with-enigma%C2%BD-bbs-709b22a1ae0d)!
* Huge shout out to [cognitivegears ](https://github.com/cognitivegears) for the various fixes, improvements, and **removing the need for cursor position reports** providing a much better terminal experience!
...and so many others! This project would be nothing without the BBS and artscene communities!
## License
Released under the [BSD 2-clause](https://opensource.org/licenses/BSD-2-Clause) license:
Copyright (c) 2015-2020, Bryan D. Ashby
Copyright (c) 2015-2022, Bryan D. Ashby
All rights reserved.
Redistribution and use in source and binary forms, with or without

View File

@ -28,6 +28,33 @@ npm install # or simply 'yarn'
Report your issue on Xibalba BBS, hop in #enigma-bbs on FreeNode and chat, or
[file a issue on GitHub](https://github.com/NuSkooler/enigma-bbs/issues).
# 0.0.12-beta to 0.0.13-beta
* :exclamation: The SSH server's `ssh2` module has gone through a major upgrade. Existing users will need to comment out two SSH KEX algorithms from their `config.hjson` if present else clients such as NetRunner will not be able to connect over SSH. Comment out `diffie-hellman-group-exchange-sha256` and `diffie-hellman-group-exchange-sha1`
* All features and changes are backwards compatible. There are a few new configuration options in a new `term` section in the configuration. These are all optional, but include the following options in case you use them:
```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
}
}
```
In addition to these, there are also new options for `term.cp437TermList` and `term.utf8TermList`. Under most circumstances these should not need to be changed. If you want to customize these lists, more information is available in `config_default.js`
# 0.0.11-beta to 0.0.12-beta
* Be aware that `master` is now mainline! This means all `git pull`'s will yield the latest version. See [WHATSNEW](WHATSNEW.md) for more information.
* **BREAKING CHANGE** There is no longer a `prompt.hjson` file. Prompts are now simply part of the menu set in the `prompts` section. If you have an existing system you will need to add your `prompt.hjson` to your `menu.hjson`'s `includes` section at a minimum. Example:

View File

@ -1,8 +1,15 @@
# Whats New
This document attempts to track **major** changes and additions in ENiGMA½. For details, see GitHub.
## 0.0.13-beta
* Removed terminal `cursor position reports` from most locations in the code. This should greatly increase the number of terminal programs that work with Enigma 1/2. For more information, see [Issue #222](https://github.com/NuSkooler/enigma-bbs/issues/222). This may also resolve other issues, such as [Issue #365](https://github.com/NuSkooler/enigma-bbs/issues/365), and [Issue #320](https://github.com/NuSkooler/enigma-bbs/issues/320). Anyone that previously had terminal incompatibilities please re-check and let us know!
* Bumped up the minimum [Node.js](https://nodejs.org/en/) version to V14. This will allow more expressive Javascript programming syntax with ECMAScript 2020 to improve the development experience.
* Added new configuration options for `term.checkUtf8Encoding`, `term.checkAnsiHomePostion`, `term.cp437TermList`, and `term.utf8TermList`. More information on these options is available in `UPGRADE.md`
* Many additional backward-compatible bug fixes since the first release of 0.0.12-beta. See the [project repository](https://github.com/NuSkooler/enigma-bbs) for more information.
## 0.0.12-beta
* The `master` branch has become mainline. What this means to users is `git pull` will always give you the latest and greatest. Make sure to read [Updating](./docs/admin/updating.md) and keep an eye on `WHATSNEW.md` (this file) and [UPGRADE](UPGRADE.md)! See also [ticket #276](https://github.com/NuSkooler/enigma-bbs/issues/276).
* Development now occurs against [Node.js 14 LTS](https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V14.md).
* The default configuration has been moved to [config_default.js](/core/config_default.js).
* A full configuration revamp has taken place. Configuration files such as `config.hjson`, `menu.hjson`, and `theme.hjson` can now utilize includes via the `includes` directive, reference 'self' sections using `@reference:` and import environment variables with `@environment`.
* An explicit prompt file previously specified by `general.promptFile` in `config.hjson` is no longer necessary. Instead, this now simply part of the `prompts` section in `menu.hjson`. The default setup still creates a separate prompt HJSON file, but it is `includes`ed in `menu.hjson`. With the removal of prompts the `PromptsChanged` event will no longer be fired.
@ -16,6 +23,11 @@ This document attempts to track **major** changes and additions in ENiGMA½. For
* `./oputil user group -group` to now accepts `~group` removing the need for special handling of the "-" character. #331
* A fix has been made to clean up old `file.db` entries when a file is removed. Previously stale records could be left or even recycled into new entries. Please see [UPGRADE.md](UPGRADE.md) for details on applying this fix (look for `tables_update_2020-11-29.sql`).
* The [./docs/modding/onelinerz.md](onelinerz) module can have `dbSuffix` set in it's `config` block to specify a separate DB file. For example to use as a requests list.
* Default hash tags can now be set in file areas. Simply supply an array or list of values in a file area block via `hashTags`.
* Added ability to pass an `env` value (map) to `abracadabra` doors. See [Local Doors](./docs/modding/local-doors.md]).
* `dropFileType` is now optional when launching doors with `abracadabra`. It can also be explicitly set to `none`.
* FSE in *view* mode can now stylize quote indicators. Supply `quoteStyleLevel1` in the `config` block. This can be a single string or an array of two strings (one to style the quotee's initials, the next for the '>' character, and finally the quoted text). See the `messageAreaViewPost` menu `config` block in the default `luciano_blocktronics` `theme.hjson` file for an example. An additional level style (e.g. for nested quotes) may be added in the future.
* FSE in *view* mode can now stylize tear lines and origin lines via `tearLineStyle` and `originStyle` `config` values in the same manor as `quoteStyleLevel`.
## 0.0.11-beta
* Upgraded from `alpha` to `beta` -- The software is far along and mature enough at this point!

View File

@ -575,6 +575,23 @@
// The 'msg_list' module looks for this entry by default
messageAreaViewPost: {
config: {
quoteStyleLevel1: [
"|00|11",
"|00|08",
"|00|03",
]
tearLineStyle: [
"|00|08",
"|00|02",
]
originStyle: [
"|00|08",
"|00|06",
"|00|03",
]
}
0: {
mci: {
TL1: {

View File

@ -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) {

View File

@ -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,6 +189,7 @@ function ANSIEscapeParser(options) {
self.emit('mci', {
position : [self.row, self.column],
mci : mciCode,
id : id ? parseInt(id, 10) : null,
args : args,
@ -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],

View File

@ -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',

View File

@ -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,6 +324,7 @@ function display(client, art, options, cb) {
mapEntry.focusArgs = mciInfo.args;
} else {
mciMap[mapKey] = {
position : mciInfo.position,
args : mciInfo.args,
SGR : mciInfo.SGR,
code : mciInfo.mci,
@ -348,9 +334,6 @@ function display(client, art, options, cb) {
if(!mciInfo.id) {
++generatedId;
}
mciCprQueue.push(mapKey);
client.term.rawWrite(ansi.queryPos());
}
});

View File

@ -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;

View File

@ -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)

View File

@ -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 = '';

View File

@ -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',
@ -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}' ],
},
},

View File

@ -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
});
},

View File

@ -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() {
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();
}
}
}
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;

View File

@ -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) {
//

View File

@ -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) {
async.waterfall(
[
function displayHeader(callback) {
theme.displayThemedAsset(
art[n],
art['header'],
self.client,
{ font : self.menuConfig.font },
function displayed(err) {
next(err);
function displayed(err, artInfo) {
return callback(err, artInfo);
}
);
}, function complete(err) {
},
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 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) {
function displayHeader(callback) {
theme.displayThemedAsset(
art[n],
art.header,
self.client,
{ font : self.menuConfig.font },
function displayed(err, artData) {
if(artData) {
mciData[n] = artData;
self[n] = { height : artData.height };
function displayed(err, artInfo) {
if(artInfo) {
mciData['header'] = artInfo;
self.header = {height: artInfo.height};
}
next(err);
return callback(err, artInfo);
}
);
}, function complete(err) {
callback(err);
},
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(callback) {
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);
let from = self.viewControllers.header.getView(MciViewIds.header.from);
if (from) {
from.acceptsFocus = false;
//from.setText(self.client.user.username);
}
// :TODO: make this a method
var body = self.viewControllers.body.getView(MciViewIds.body.message);
@ -774,11 +848,13 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
{
const fromView = self.viewControllers.header.getView(MciViewIds.header.from);
const area = getMessageAreaByTag(self.messageAreaTag);
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) {
self.initHeaderReplyEditMode();
@ -863,7 +939,10 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
}
initHeaderViewMode() {
// 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
View 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;
};

View File

@ -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));

View File

@ -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');

View File

@ -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) {

View File

@ -39,13 +39,13 @@ function MenuView(options) {
this.focusedItemIndex = this.items.length >= this.focusedItemIndex ? this.focusedItemIndex : 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) {

View File

@ -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)

View File

@ -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': {

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -27,7 +27,6 @@ const _ = require('lodash');
const fs = require('graceful-fs');
const paths = require('path');
const moment = require('moment');
const async = require('async');
const ModuleInfo = exports.moduleInfo = {
name : 'Gopher',
@ -89,7 +88,15 @@ exports.getModule = class GopherModule extends ServerModule {
socket.setEncoding('ascii');
socket.on('data', data => {
// 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 => {

View File

@ -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
});
}

View File

@ -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);

View File

@ -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) {

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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(),

View File

@ -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));

View File

@ -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) {

View File

@ -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();
};

View File

@ -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.

56
docker/Dockerfile Normal file
View File

@ -0,0 +1,56 @@
FROM node:12-buster-slim
LABEL maintainer="dave@force9.org"
ENV NVM_DIR /root/.nvm
ENV DEBIAN_FRONTEND noninteractive
COPY . /enigma-bbs
# Do some installing! (and alot of cleaning up) keeping it in one step for less docker layers
# - if you need to debug i recommend to break the steps with individual RUNs)
RUN apt-get update \
&& apt-get install -y \
git \
curl \
build-essential \
python \
python3 \
libssl-dev \
lrzsz \
arj \
lhasa \
unrar-free \
p7zip-full \
&& npm install -g pm2 \
&& cd /enigma-bbs && npm install --only=production \
&& pm2 start main.js \
&& mkdir -p /enigma-bbs-pre/art \
&& mkdir /enigma-bbs-pre/mods \
&& mkdir /enigma-bbs-pre/config \
&& cp -rp art/* ../enigma-bbs-pre/art/ \
&& cp -rp mods/* ../enigma-bbs-pre/mods/ \
&& cp -rp config/* ../enigma-bbs-pre/config/ \
&& apt-get remove build-essential python python3 libssl-dev git curl -y \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
&& apt-get clean
# sexyz
COPY docker/bin/sexyz /usr/local/bin
RUN chmod +x /enigma-bbs/docker/bin/docker-entrypoint.sh
# enigma storage mounts
VOLUME /enigma-bbs/art
VOLUME /enigma-bbs/config
VOLUME /enigma-bbs/db
VOLUME /enigma-bbs/filebase
VOLUME /enigma-bbs/logs
VOLUME /enigma-bbs/mods
VOLUME /mail
# Enigma default port
EXPOSE 8888
WORKDIR /enigma-bbs
ENTRYPOINT ["/enigma-bbs/docker/bin/docker-entrypoint.sh"]

View File

@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -e
# Set some vars
PRE_POPULATED_VOLUMES=("config" "mods" "art") # These are folders which contain runtime needed files, and need to be represented in the host
BBS_ROOT_DIR=/enigma-bbs # Install location
BBS_STAGING_PATH=/enigma-bbs-pre # Staging location for pre populated volumes (PRE_POPULATED_VOLUMES)
CONFIG_NAME=config.hjson # This is the default name, this script is intended for easy get-go - make changes as needed
# Setup happens when there is no existing config file
if [[ ! -f $BBS_ROOT_DIR/config/$CONFIG_NAME ]]; then
for VOLUME in "${PRE_POPULATED_VOLUMES[@]}"
do
if [ -n "$(find "$BBS_ROOT_DIR/$VOLUME" -maxdepth 0 -type d -empty 2>/dev/null)" ]; then
cp -rp $BBS_STAGING_PATH/$VOLUME/* $BBS_ROOT_DIR/$VOLUME/
else
printf "WARN: skipped $BBS_ROOT_DIR/$VOLUME: Volume not empty or not a new setup; Files required to run ENiGMA 1/2 may be missing.\n Possible bad state\n"
printf "INFO: You have mounted folders with existing data - but no existing config json.\n\nPossible solutions:\n1. Make sure all volumes are set correctly specifically config volume... \n2. Check your configuration name if non-default\n\n\n"
fi
done
./oputil.js config new
fi
if [[ ! -f $BBS_ROOT_DIR/config/$CONFIG_NAME ]]; then # Make sure once more, otherwise pm2-runtime will loop if missing the config
printf "ERROR: Missing configuration - ENiGMA 1/2 will not work. please run config\n"
exit 1
else
exec pm2-runtime main.js
fi

BIN
docker/bin/sexyz Executable file

Binary file not shown.

View File

@ -8,10 +8,10 @@ source "https://rubygems.org"
#
# This will help ensure the proper Jekyll version is running.
# Happy Jekylling!
gem "jekyll", "~> 3.7.0"
gem "jekyll", "~> 4.2.1"
# This is the default theme for new Jekyll sites. You may change this to anything you like.
gem "hacker"
# gem "hacker"
# If you want to use GitHub Pages, remove the "gem "jekyll"" above and
# uncomment the line below. To upgrade, run `bundle update github-pages`.
@ -19,11 +19,12 @@ gem "hacker"
# If you have any plugins, put them here!
group :jekyll_plugins do
gem "jekyll-feed", "~> 0.6"
gem 'jekyll-seo-tag'
gem 'jekyll-theme-hacker'
gem 'jekyll-sitemap'
gem 'jemoji'
gem 'jekyll-seo-tag', '~> 2.7.1'
gem 'jekyll-theme-hacker', '~>0.2.0'
gem 'jekyll-sitemap', '~>1.4.0'
gem 'jemoji', '~>0.12.0'
gem 'jekyll-relative-links', '~>0.6.1'
gem 'jekyll-minifier'
end
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem

View File

@ -1,101 +1,116 @@
GEM
remote: https://rubygems.org/
specs:
activesupport (4.2.9)
i18n (~> 0.7)
minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
addressable (2.5.2)
public_suffix (>= 2.0.2, < 4.0)
activesupport (7.0.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
colorator (1.1.0)
concurrent-ruby (1.0.5)
em-websocket (0.5.1)
concurrent-ruby (1.1.9)
cssminify2 (2.0.1)
em-websocket (0.5.3)
eventmachine (>= 0.12.9)
http_parser.rb (~> 0.6.0)
eventmachine (1.2.5)
ffi (1.9.24)
http_parser.rb (~> 0)
eventmachine (1.2.7)
execjs (2.8.1)
ffi (1.15.5)
forwardable-extended (2.6.0)
gemoji (3.0.0)
hacker (0.0.1)
html-pipeline (2.7.1)
gemoji (3.0.1)
html-pipeline (2.14.0)
activesupport (>= 2)
nokogiri (>= 1.8.5)
http_parser.rb (0.6.0)
i18n (0.9.1)
nokogiri (>= 1.4)
htmlcompressor (0.4.0)
http_parser.rb (0.8.0)
i18n (1.9.1)
concurrent-ruby (~> 1.0)
jekyll (3.7.4)
jekyll (4.2.1)
addressable (~> 2.4)
colorator (~> 1.0)
em-websocket (~> 0.5)
i18n (~> 0.7)
jekyll-sass-converter (~> 1.0)
i18n (~> 1.0)
jekyll-sass-converter (~> 2.0)
jekyll-watch (~> 2.0)
kramdown (~> 1.14)
kramdown (~> 2.3)
kramdown-parser-gfm (~> 1.0)
liquid (~> 4.0)
mercenary (~> 0.3.3)
mercenary (~> 0.4.0)
pathutil (~> 0.9)
rouge (>= 1.7, < 4)
rouge (~> 3.0)
safe_yaml (~> 1.0)
jekyll-feed (0.9.2)
jekyll (~> 3.3)
jekyll-sass-converter (1.5.1)
sass (~> 3.4)
jekyll-seo-tag (2.4.0)
jekyll (~> 3.3)
jekyll-sitemap (1.1.1)
jekyll (~> 3.3)
jekyll-theme-hacker (0.1.0)
jekyll (~> 3.5)
terminal-table (~> 2.0)
jekyll-minifier (0.1.10)
cssminify2 (~> 2.0)
htmlcompressor (~> 0.4)
jekyll (>= 3.5)
json-minify (~> 0.0.3)
uglifier (~> 4.1)
jekyll-relative-links (0.6.1)
jekyll (>= 3.3, < 5.0)
jekyll-sass-converter (2.1.0)
sassc (> 2.0.1, < 3.0)
jekyll-seo-tag (2.7.1)
jekyll (>= 3.8, < 5.0)
jekyll-sitemap (1.4.0)
jekyll (>= 3.7, < 5.0)
jekyll-theme-hacker (0.2.0)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-watch (2.0.0)
jekyll-watch (2.2.1)
listen (~> 3.0)
jemoji (0.8.1)
activesupport (~> 4.0, >= 4.2.9)
jemoji (0.12.0)
gemoji (~> 3.0)
html-pipeline (~> 2.2)
jekyll (>= 3.0)
kramdown (1.16.2)
liquid (4.0.0)
listen (3.1.5)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
ruby_dep (~> 1.2)
mercenary (0.3.6)
mini_portile2 (2.4.0)
minitest (5.11.1)
nokogiri (1.10.8)
mini_portile2 (~> 2.4.0)
pathutil (0.16.1)
jekyll (>= 3.0, < 5.0)
json (2.6.1)
json-minify (0.0.3)
json (> 0)
kramdown (2.3.1)
rexml
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
liquid (4.0.3)
listen (3.7.1)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
mercenary (0.4.0)
minitest (5.15.0)
nokogiri (1.13.1-x86_64-linux)
racc (~> 1.4)
pathutil (0.16.2)
forwardable-extended (~> 2.6)
public_suffix (3.0.1)
rb-fsevent (0.10.2)
rb-inotify (0.9.10)
ffi (>= 1.9.24, < 2)
rouge (3.1.0)
ruby_dep (1.5.0)
safe_yaml (1.0.4)
sass (3.5.5)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
thread_safe (0.3.6)
tzinfo (1.2.4)
thread_safe (~> 0.1)
public_suffix (4.0.6)
racc (1.6.0)
rb-fsevent (0.11.0)
rb-inotify (0.10.1)
ffi (~> 1.0)
rexml (3.2.5)
rouge (3.28.0)
safe_yaml (1.0.5)
sassc (2.4.0)
ffi (~> 1.9)
terminal-table (2.0.0)
unicode-display_width (~> 1.1, >= 1.1.1)
tzinfo (2.0.4)
concurrent-ruby (~> 1.0)
uglifier (4.2.0)
execjs (>= 0.3.0, < 3)
unicode-display_width (1.8.0)
PLATFORMS
ruby
x86_64-linux
DEPENDENCIES
hacker
jekyll (~> 3.7.0)
jekyll-feed (~> 0.6)
jekyll-seo-tag
jekyll-sitemap
jekyll-theme-hacker
jemoji
jekyll (~> 4.2.1)
jekyll-minifier
jekyll-relative-links (~> 0.6.1)
jekyll-seo-tag (~> 2.7.1)
jekyll-sitemap (~> 1.4.0)
jekyll-theme-hacker (~> 0.2.0)
jemoji (~> 0.12.0)
tzinfo-data
BUNDLED WITH
1.16.1
2.3.5

View File

@ -9,13 +9,17 @@ logo: /assets/images/enigma-logo.png
markdown: kramdown
theme: jekyll-theme-hacker
plugins:
- jekyll-feed
- jekyll-seo-tag
- jekyll-relative-links
- jekyll-sitemap
- jemoji
baseurl: /enigma-bbs
relative_links:
enabled: true
collections: true
# Exclude from processing.
# The following items will not be processed, by default. Create a custom list
# to override the default setting.
@ -28,3 +32,102 @@ exclude:
- vendor/gems/
- vendor/ruby/
- .idea
# New documents that are not included below under order will display at the
# end of the list. Section names for directories and subdirectories are
# setup in _data/sections.yml. Change there in order to update the name of
# one of the subdirectories or to add a new one.
collections:
docs:
output: true
permalink: /:path:output_ext
order:
- installation/installation-methods.md
- installation/install-script.md
- installation/docker.md
- installation/manual.md
- installation/hardware/rpi.md
- installation/hardware/windows.md
- installation/network.md
- installation/testing.md
- installation/production.md
- configuration/creating-config.md
- configuration/sysop-setup.md
- configuration/config-files.md
- configuration/config-hjson.md
- configuration/hjson.md
- configuration/menu-hjson.md
- configuration/directory-structure.md
- configuration/external-binaries.md
- configuration/archivers.md
- configuration/file-transfer-protocols.md
- configuration/email.md
- configuration/colour-codes.md
- configuration/event-scheduler.md
- configuration/acs.md
- configuration/security.md
- misc/user-interrupt.md
- filebase/index.md
- filebase/first-file-area.md
- filebase/acs.md
- filebase/uploads.md
- filebase/web-access.md
- filebase/tic-support.md
- filebase/network-mounts-and-symlinks.md
- messageareas/configuring-a-message-area.md
- messageareas/message-networks.md
- messageareas/bso-import-export.md
- messageareas/netmail.md
- messageareas/qwk.md
- messageareas/ftn.md
- art/general.md
- art/themes.md
- art/mci.md
- art/views/button_view.md
- art/views/edit_text_view.md
- art/views/full_menu_view.md
- art/views/horizontal_menu_view.md
- art/views/mask_edit_text_view.md
- art/views/multi_line_edit_text_view.md
- art/views/predefined_label_view.md
- art/views/spinner_menu_view.md
- art/views/text_view.md
- art/views/toggle_menu_view.md
- art/views/vertical_menu_view.md
- servers/loginservers/telnet.md
- servers/loginservers/ssh.md
- servers/loginservers/websocket.md
- servers/contentservers/web-server.md
- servers/contentservers/gopher.md
- servers/contentservers/nntp.md
- modding/local-doors.md
- modding/door-servers.md
- modding/telnet-bridge.md
- modding/existing-mods.md
- modding/file-area-list.md
- modding/last-callers.md
- modding/whos-online.md
- modding/user-list.md
- modding/msg-conf-list.md
- modding/msg-area-list.md
- modding/bbs-list.md
- modding/rumorz.md
- modding/file-transfer-protocol-select.md
- modding/onelinerz.md
- modding/show-art.md
- modding/file-base-download-manager.md
- modding/file-base-web-download-manager.md
- modding/set-newscan-date.md
- modding/node-msg.md
- modding/top-x.md
- modding/user-2fa-otp-config.md
- modding/autosig-edit.md
- modding/menu-modules.md
- admin/administration.md
- admin/oputil.md
- admin/updating.md
- troubleshooting/monitoring-logs.md

28
docs/_data/sections.yml Normal file
View File

@ -0,0 +1,28 @@
installation:
title: Installation
configuration:
title: Configuration
filebase:
title: File Base
messageareas:
title: Message Areas
art:
title: Art
servers:
title: Servers
modding:
title: Modding
admin:
title: Administration
troubleshooting:
title: Troubleshooting
misc:
title: Miscellaneous
views:
title: Views
hardware:
title: OS / Hardware Specific
loginservers:
title: Login Servers
contentservers:
title: Content Servers

View File

@ -128,15 +128,17 @@ a Vertical Menu (`%VM`): Old-school BBSers may recognize this as a lightbar menu
| Code | Name | Description | Notes |
|------|----------------------|------------------|-------|
| `TL` | Text Label | Displays text | Static content |
| `ET` | Edit Text | Collect user input | Single line entry |
| `ME` | Masked Edit Text | Collect user input using a *mask* | See **Mask Edits** below |
| `MT` | Multi Line Text Edit | Multi line edit control | Used for FSE, display of FILE_ID.DIZ, etc. |
| `BT` | Button | A button | ...it's a button |
| `VM` | Vertical Menu | A vertical menu | AKA a vertical lightbar; Useful for lists |
| `HM` | Horizontal Menu | A horizontal menu | AKA a horizontal lightbar |
| `SM` | Spinner Menu | A spinner input control | Select *one* from multiple options |
| `TM` | Toggle Menu | A toggle menu | Commonly used for Yes/No style input |
| `TL` | Text Label | Displays text | Static content. See [Text View](views/text_view.md) |
| `ET` | Edit Text | Collect user input | Single line entry. See [Edit Text](views/edit_text_view.md) |
| `ME` | Masked Edit Text | Collect user input using a *mask* | See [Masked Edit](views/mask_edit_text_view.md) and **Mask Edits** below. |
| `MT` | Multi Line Text Edit | Multi line edit control | Used for FSE, display of FILE_ID.DIZ, etc. See [Multiline Text Edit](views/multi_line_edit_text_view.md) |
| `BT` | Button | A button | ...it's a button. See [Button](views/button_view.md) |
| `VM` | Vertical Menu | A vertical menu | AKA a vertical lightbar; Useful for lists. See [Vertical Menu](views/vertical_menu_view.md) |
| `HM` | Horizontal Menu | A horizontal menu | AKA a horizontal lightbar. See [Horizontal Menu](views/horizontal_menu_view.md) |
| `FM` | Full Menu | A menu that can go both vertical and horizontal. | See [Full Menu](views/full_menu_view.md) |
| `SM` | Spinner Menu | A spinner input control | Select *one* from multiple options. See [Spinner Menu](views/spinner_menu_view.md) |
| `TM` | Toggle Menu | A toggle menu | Commonly used for Yes/No style input. See [Toggle Menu](views/toggle_menu_view.md)|
| `PL` | Predefined Label | Show environment information | See [Predefined Label](views/predefined_label_view.md)|
| `KE` | Key Entry | A *single* key input control | Think hotkeys |
:information_source: Peek at [/core/mci_view_factory.js](https://github.com/NuSkooler/enigma-bbs/blob/master/core/mci_view_factory.js) to see additional information.

View File

@ -0,0 +1,57 @@
---
layout: page
title: Button View
---
## Button View
A button view supports displaying a button on a screen.
## General Information
:information_source: A button view is defined with a percent (%) and the characters BT, followed by the view number. For example: `%BT1`
:information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them.
### Properties
| Property | Description |
|-------------|--------------|
| `text` | Sets the text to display on the button |
| `textStyle` | Sets the standard (non-focus) text style. See **Text Styles** in [MCI](../mci.md) |
| `focusTextStyle` | Sets focus text style. See **Text Styles** in [MCI](../mci.md)|
| `width` | Sets the width of a view to display one or more columns horizontally (default 15)|
| `focus` | If set to `true`, establishes initial focus |
| `submit` | If set to `true` any `accept` action upon this view will submit the encompassing **form** |
| `argName` | Sets the argument name for this selection in the form |
| `justify` | Sets the justification of each item in the list. Options: left (default), right, center |
| `fillChar` | Specifies a character to fill extra space longer than the text length. Defaults to an empty space |
| `textOverflow` | If the button text cannot be displayed due to `width`, set overflow characters. See **Text Overflow** below |
### Text Overflow
The `textOverflow` option is used to specify what happens when a text string is too long to fit in the `width` defined.
:information_source: If `textOverflow` is not specified at all, a button can become wider than the `width` if needed to display the text value.
:information_source: Setting `textOverflow` to an empty string `textOverflow: ""` will cause the item to be truncated if necessary without any characters displayed
:information_source: Otherwise, setting `textOverflow` to one or more characters will truncate the value if necessary and display those characters at the end. i.e. `textOverflow: ...`
## Example
![Example](../../assets/images/button_view_example1.gif "Button")
<details>
<summary>Configuration fragment (expand to view)</summary>
<div markdown="1">
```
BT1: {
submit: true
justify: center
argName: btnSelect
width: 17
focusTextStyle: upper
text: Centered button
}
```
</div>
</details>

View File

@ -0,0 +1,42 @@
---
layout: page
title: Edit Text View
---
## Edit Text View
An edit text view supports editing form values on a screen. This can be for new entry as well as editing existing values defined by the module.
## General Information
:information_source: An edit text view is defined with a percent (%) and the characters ET, followed by the view number. For example: `%ET1`. This is generally used on a form in order to allow a user to enter or edit a text value.
:information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them.
### Properties
| Property | Description |
|-------------|--------------|
| `textStyle` | Sets the standard (non-focus) text style. See **Text Styles** in [MCI](../mci.md) |
| `focusTextStyle` | Sets the focus text style. See **Text Styles** in [MCI](../mci.md) |
| `width` | Sets the width of a view for the text edit (default 15)|
| `argName` | Sets the argument name for this value in the form |
| `maxLength` | Sets the maximum number of characters that can be entered |
| `focus` | Set to true to capture initial focus |
| `justify` | Sets the justification of the text entry. Options: left (default), right, center |
| `fillChar` | Specifies a character to fill extra space in the text entry with. Defaults to an empty space |
## Example
![Example](../../assets/images/edit_text_view_example1.gif "Edit Text View")
<details>
<summary>Configuration fragment (expand to view)</summary>
<div markdown="1">
```
ET1: {
maxLength: @config:users.usernameMax
argName: username
focus: true
}
```
</div>
</details>

View File

@ -0,0 +1,240 @@
---
layout: page
title: Full Menu View
---
## Full Menu View
A full menu view supports displaying a list of times on a screen in a very configurable manner. A full menu view supports either a single row or column of values, similar to Horizontal Menu (HM) and Vertical Menu (VM), or in multiple columns.
## General Information
Items can be selected on a menu via the cursor keys, Page Up, Page Down, Home, and End, or by selecting them via a `hotKey` - see ***Hot Keys*** below.
:information_source: A full menu view is defined with a percent (%) and the characters FM, followed by the view number. For example: `%FM1`
:information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them.
### Properties
| Property | Description |
|-------------|--------------|
| `textStyle` | Sets the standard (non-focus) text style. See **Text Styles** in [MCI](../mci.md) |
| `focusTextStyle` | Sets focus text style. See **Text Styles** in [MCI](../mci.md)|
| `itemSpacing` | Used to separate items vertically in the menu |
| `itemHorizSpacing` | Used to separate items horizontally in the menu |
| `height` | Sets the height of views to display multiple items vertically (default 1) |
| `width` | Sets the width of a view to display one or more columns horizontally (default 15)|
| `focus` | If set to `true`, establishes initial focus |
| `submit` | If set to `true` any `accept` action upon this view will submit the encompassing **form** |
| `hotKeys` | Sets hot keys to activate specific items. See **Hot Keys** below |
| `hotKeySubmit` | Set to submit a form on hotkey selection |
| `argName` | Sets the argument name for this selection in the form |
| `justify` | Sets the justification of each item in the list. Options: left (default), right, center |
| `itemFormat` | Sets the format for a list entry. See **Entry Formatting** in [MCI](../mci.md) |
| `fillChar` | Specifies a character to fill extra space in the menu with. Defaults to an empty space |
| `textOverflow` | If a single column cannot be displayed due to `width`, set overflow characters. See **Text Overflow** below |
| `items` | List of items to show in the menu. See **Items** below.
| `focusItemFormat` | Sets the format for a focused list entry. See **Entry Formatting** in [MCI](../mci.md) |
### Hot Keys
A set of `hotKeys` are used to allow the user to press a character on the keyboard to select that item, and optionally submit the form.
Example:
```
hotKeys: { A: 0, B: 1, C: 2, D: 3 }
hotKeySubmit: true
```
This would select and submit the first item if `A` is typed, second if `B`, etc.
### Items
A full menu, similar to other menus, take a list of items to display in the menu. For example:
```
items: [
{
text: First Item
data: first
}
{
text: Second Item
data: second
}
]
```
If the list is for display only (there is no form action associated with it) you can omit the data element, and include the items as a simple list:
```
["First item", "Second item", "Third Item"]
```
### Text Overflow
The `textOverflow` option is used to specify what happens when a text string is too long to fit in the `width` defined. Note, because columns are automatically calculated, this can only occur when the text is too long to fit the `width` using a single column.
:information_source: If `textOverflow` is not specified at all, a menu can become wider than the `width` if needed to display a single column.
:information_source: Setting `textOverflow` to an empty string `textOverflow: ""` will cause the item to be truncated if necessary without any characters displayed
:information_source: Otherwise, setting `textOverflow` to one or more characters will truncate the value if necessary and display those characters at the end. i.e. `textOverflow: ...`
## Examples
### A simple vertical menu - similar to VM
![Example](../../assets/images/full_menu_view_example1.gif "Vertical menu")
<details>
<summary>Configuration fragment (expand to view)</summary>
<div markdown="1">
```
FM1: {
submit: true
argName: navSelect
width: 1
items: [
{
text: login
data: login
}
{
text: apply
data: new user
}
{
text: about
data: about
}
{
text: log off
data: logoff
}
]
}
```
</div>
</details>
### A simple horizontal menu - similar to HM
![Example](../../assets/images/full_menu_view_example2.gif "Horizontal menu")
<details>
<summary>Configuration fragment (expand to view)</summary>
<div markdown="1">
```
FM2: {
focus: true
height: 1
width: 60 // set as desired
submit: true
argName: navSelect
items: [
"prev", "next", "details", "toggle queue", "rate", "help", "quit"
]
}
```
</div>
</details>
### A multi-column navigation menu with hotkeys
![Example](../../assets/images/full_menu_view_example3.gif "Multi column menu")
<details>
<summary>Configuration fragment (expand to view)</summary>
<div markdown="1">
```
FM1: {
focus: true
height: 6
width: 60
submit: true
argName: navSelect
hotKeys: { M: 0, E: 1, D: 2 ,F: 3,!: 4, A: 5, C: 6, Y: 7, S: 8, R: 9, O: 10, L:11, U:12, W: 13, B:14, G:15, T: 16, Q:17 }
hotKeySubmit: true
items: [
{
text: M) message area
data: message
}
{
text: E) private email
data: email
}
{
text: D) doors
data: doors
}
{
text: F) file base
data: files
}
{
text: !) global newscan
data: newscan
}
{
text: A) achievements
data: achievements
}
{
text: C) configuration
data: config
}
{
text: Y) user stats
data: userstats
}
{
text: S) system stats
data: systemstats
}
{
text: R) rumorz
data: rumorz
}
{
text: O) onelinerz
data: onelinerz
}
{
text: L) last callers
data: callers
}
{
text: U) user list
data: userlist
}
{
text: W) whos online
data: who
}
{
text: B) bbs list
data: bbslist
}
{
text: G) node-to-node messages
data: nodemessages
}
{
text: T) multi relay chat
data: mrc
}
{
text: Q) quit
data: quit
}
]
}
```
</div>
</details>

View File

@ -0,0 +1,91 @@
---
layout: page
title: Horizontal Menu View
---
## Horizontal Menu View
A horizontal menu view supports displaying a list of times on a screen horizontally (side to side, in a single row) similar to a lightbox.
## General Information
Items can be selected on a menu via the cursor keys, Page Up, Page Down, Home, and End, or by selecting them via a `hotKey` - see ***Hot Keys*** below.
:information_source: A horizontal menu view is defined with a percent (%) and the characters HM, followed by the view number (if used.) For example: `%HM1`
:information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them.
### Properties
| Property | Description |
|-------------|--------------|
| `textStyle` | Sets the standard (non-focus) text style. See **Text Styles** in [MCI](../mci.md) |
| `focusTextStyle` | Sets focus text style. See **Text Styles** in [MCI](../mci.md)|
| `itemSpacing` | Used to separate items horizontally in the menu |
| `width` | Sets the width of a view to display one or more columns horizontally (default 15)|
| `focus` | If set to `true`, establishes initial focus |
| `submit` | If set to `true` any `accept` action upon this view will submit the encompassing **form** |
| `hotKeys` | Sets hot keys to activate specific items. See **Hot Keys** below |
| `hotKeySubmit` | Set to submit a form on hotkey selection |
| `argName` | Sets the argument name for this selection in the form |
| `justify` | Sets the justification of each item in the list. Options: left (default), right, center |
| `itemFormat` | Sets the format for a list entry. See **Entry Formatting** in [MCI](../mci.md) |
| `fillChar` | Specifies a character to fill extra space in the menu with. Defaults to an empty space |
| `items` | List of items to show in the menu. See **Items** below.
| `focusItemFormat` | Sets the format for a focused list entry. See **Entry Formatting** in [MCI](../mci.md) |
### Hot Keys
A set of `hotKeys` are used to allow the user to press a character on the keyboard to select that item, and optionally submit the form.
Example:
```
hotKeys: { A: 0, B: 1, C: 2, D: 3 }
hotKeySubmit: true
```
This would select and submit the first item if `A` is typed, second if `B`, etc.
### Items
A horizontal menu, similar to other menus, take a list of items to display in the menu. For example:
```
items: [
{
text: First Item
data: first
}
{
text: Second Item
data: second
}
]
```
If the list is for display only (there is no form action associated with it) you can omit the data element, and include the items as a simple list:
```
["First item", "Second item", "Third Item"]
```
## Example
![Example](../../assets/images/horizontal_menu_view_example1.gif "Horizontal menu")
<details>
<summary>Configuration fragment (expand to view)</summary>
<div markdown="1">
```
HM2: {
focus: true
width: 60 // set as desired
submit: true
argName: navSelect
items: [
"prev", "next", "details", "toggle queue", "rate", "help", "quit"
]
}
```
</div>
</details>

View File

@ -0,0 +1,64 @@
---
layout: page
title: Mask Edit Text View
---
## Mask Edit Text View
A mask edit text view supports editing form values on a screen. This can be for new entry as well as editing existing values. Unlike a edit text view, the mask edit text view uses a mask pattern to specify what format the values should be entered in.
## General Information
:information_source: A mask edit text view is defined with a percent (%) and the characters ME, followed by the view number. For example: `%ME1`. This is generally used on a form in order to allow a user to enter or edit a text value.
:information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them.
### Properties
| Property | Description |
|-------------|--------------|
| `textStyle` | Sets the standard (non-focus) text style. See **Text Styles** in [MCI](../mci.md) |
| `focusTextStyle` | Sets the focus text style. See **Text Styles** in [MCI](../mci.md) |
| `argName` | Sets the argument name for this value in the form |
| `maxLength` | Sets the maximum number of characters that can be entered. *Not normally useful, set the mask pattern as needed instead* |
| `focus` | Set to true to capture initial focus |
| `maskPattern` | Sets the mask pattern. See **Mask Pattern** below |
| `fillChar` | Specifies a character to fill extra space in the text entry with. Defaults to an empty space |
### Mask Pattern
A `maskPattern` must be set on a mask edit text view (not doing so will cause the view to be focusable, but no text can be input). The `maskPattern` is a set of characters used to define input, as well as optional literal characters that can be entered into the pattern that will always be entered into the input. The following mask characters are supported:
| Mask Character | Description |
|----------------|--------------|
| # | Numeric input, one of 0 through 9 |
| A | Alphabetic, one of a through z or A through Z |
| @ | Alphanumeric, matches one of either Numeric or Alphabetic above |
| & | Printable, matches one printable character including spaces |
Any value other than the entries above is treated like a literal value to be displayed in the patter. Multiple pattern characters are combined for longer inputs. Some examples could include:
| Pattern | Description |
|---------|--------------|
| `AA` | Matches up to two alphabetic characters, for example a state name (i.e. "CA") |
| `###` | Matches up to three numeric characters, for example an age (i.e. 25) |
| `###-###-####` | A pattern matching a phone number with area code |
| `##/##/####` | Matches a date of type month/day/year or day/month/year (i.e. 01/01/2000) |
| `##-AAA-####` | Matches a date of type day-month-year (i.e. 01-MAR-2010) |
| `# foot ## inches`| Matches a height in feet and inches (i.e. 6 foot 2 inches) |
## Example
![Example](../../assets/images/mask_edit_text_view_example1.gif "Masked Text Edit View")
<details>
<summary>Configuration fragment (expand to view)</summary>
<div markdown="1">
```
ME1: {
argName: height
fillChar: "#"
maskPattern: "# ft. ## in."
}
```
</div>
</details>

View File

@ -0,0 +1,53 @@
---
layout: page
title: Multi Line Edit Text View
---
## Multi Line Edit Text View
A text display / editor designed to edit or display a message.
## General Information
:information_source: A multi line edit text view is defined with a percent (%) and the characters MT, followed by the view number. For example: `%MT1`
:information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them.
### Properties
| Property | Description |
|-------------|--------------|
| `text` | Sets the text to display - only useful for read-only and preview, otherwise use a specific module |
| `width` | Sets the width of a view to display horizontally (default 15) |
| `height` | Sets the height of a view to display vertically |
| `argName` | Sets the argument name for the form |
| `mode` | One of edit, preview, or read-only. See **Mode** below |
### Mode
The mode of a multi line edit text view controls how the view behaves. The following modes are allowed:
| Mode | Description |
|-------------|--------------|
| edit | edit the contents of the view |
| preview | preview the text, including scrolling |
| read-only | No scrolling or editing the view |
:information_source: If `mode` is not set, the default mode is "edit"
:information_source: With mode preview, scrolling the contents is allowed, but is not with read-only.
## Example
![Example](../../assets/images/multi_line_edit_text_view_example1.gif "Multi Line Edit Text View")
<details>
<summary>Configuration fragment (expand to view)</summary>
<div markdown="1">
```
ML1: {
width: 79
argName: message
mode: edit
}
```
</div>
</details>

View File

@ -0,0 +1,49 @@
---
layout: page
title: Predefined Label View
---
## Predefined Label View
A predefined label view supports displaying a predefined MCI label on a screen.
## General Information
:information_source: A predefined label view is defined with a percent (%) and the characters PL, followed by the view number and then the predefined MCI value in parenthesis. For example: `%PL1(VL)` to display the Version Label. *NOTE*: this is an alternate way of placing MCI codes, as the MCI can also be placed on the art page directly with the code. For example `%VL`. The difference between these is that the PL version can have additional formatting options applied to it.
:information_source: See *Predefined Codes* in [MCI](../mci.md) for the list of available MCI codes.
:information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them.
### Properties
| Property | Description |
|-------------|--------------|
| `textStyle` | Sets the standard (non-focus) text style. See **Text Styles** in [MCI](../mci.md) |
| `justify` | Sets the justification of the MCI value text. Options: left (default), right, center |
| `fillChar` | Specifies a character to fill extra space in the view. Defaults to an empty space |
| `width` | Specifies the width that the value should be displayed in (default 3) |
| `textOverflow` | If the MCI is wider than width, set overflow characters. See **Text Overflow** below |
### Text Overflow
The `textOverflow` option is used to specify what happens when a predefined MCI string is too long to fit in the `width` defined.
:information_source: If `textOverflow` is not specified at all, a predefined label view can become wider than the `width` if needed to display the MCI value.
:information_source: Setting `textOverflow` to an empty string `textOverflow: ""` will cause the item to be truncated if necessary without any characters displayed
:information_source: Otherwise, setting `textOverflow` to one or more characters will truncate the value if necessary and display those characters at the end. i.e. `textOverflow: ...`
## Example
![Example](../../assets/images/predefined_label_view_example1.png "Predefined label")
<details>
<summary>Configuration fragment (expand to view)</summary>
<div markdown="1">
```
PL1: {
textStyle: upper
}
```
</div>
</details>

View File

@ -0,0 +1,104 @@
---
layout: page
title: Spinner Menu View
---
## Spinner Menu View
A spinner menu view supports displaying a set of times on a screen as a list, with one item displayed at a time. This is generally used to pick one option from a list. Some examples could include selecting from a list of states, themes, etc.
## General Information
Items can be selected on a menu via the cursor keys or by selecting them via a `hotKey` - see ***Hot Keys*** below.
:information_source: A spinner menu view is defined with a percent (%) and the characters SM, followed by the view number (if used.) For example: `%SM1`
:information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them.
### Properties
| Property | Description |
|-------------|--------------|
| `textStyle` | Sets the standard (non-focus) text style. See **Text Styles** in [MCI](../mci.md) |
| `focusTextStyle` | Sets focus text style. See **Text Styles** in [MCI](../mci.md)|
| `focus` | If set to `true`, establishes initial focus |
| `width` | Sets the width of a view on the display (default 15)|
| `submit` | If set to `true` any `accept` action upon this view will submit the encompassing **form** |
| `hotKeys` | Sets hot keys to activate specific items. See **Hot Keys** below |
| `hotKeySubmit` | Set to submit a form on hotkey selection |
| `argName` | Sets the argument name for this selection in the form |
| `justify` | Sets the justification of each item in the list. Options: left (default), right, center |
| `itemFormat` | Sets the format for a list entry. See **Entry Formatting** in [MCI](../mci.md) |
| `fillChar` | Specifies a character to fill extra space in the menu with. Defaults to an empty space |
| `items` | List of items to show in the menu. See **Items** below.
| `focusItemFormat` | Sets the format for a focused list entry. See **Entry Formatting** in [MCI](../mci.md) |
### Hot Keys
A set of `hotKeys` are used to allow the user to press a character on the keyboard to select that item, and optionally submit the form.
Example:
```
hotKeys: { A: 0, B: 1, C: 2, D: 3 }
hotKeySubmit: true
```
This would select and submit the first item if `A` is typed, second if `B`, etc.
### Items
A spinner menu, similar to other menus, take a list of items to display in the menu. For example:
```
items: [
{
text: First Item
data: first
}
{
text: Second Item
data: second
}
]
```
If the list is for display only (there is no form action associated with it) you can omit the data element, and include the items as a simple list:
```
["First item", "Second item", "Third Item"]
```
## Example
![Example](../../assets/images/spinner_menu_view_example1.gif "Spinner menu")
<details>
<summary>Configuration fragment (expand to view)</summary>
<div markdown="1">
```
SM1: {
submit: true
argName: themeSelect
items: [
{
text: Light
data: light
}
{
text: Dark
data: dark
}
{
text: Rainbow
data: rainbow
}
{
text: Gruvbox
data: gruvbox
}
]
}
```
</div>
</details>

View File

@ -0,0 +1,48 @@
---
layout: page
title: Text View
---
## Text View
A text label view supports displaying simple text on a screen.
## General Information
:information_source: A text label view is defined with a percent (%) and the characters TL, followed by the view number. For example: `%TL1`
:information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them.
### Properties
| Property | Description |
|-------------|--------------|
| `text` | Sets the text to display on the label |
| `textStyle` | Sets the standard (non-focus) text style. See **Text Styles** in [MCI](../mci.md) |
| `width` | Sets the width of a view to display horizontally (default 15)|
| `justify` | Sets the justification of the text in the view. Options: left (default), right, center |
| `fillChar` | Specifies a character to fill extra space in the view with. Defaults to an empty space |
| `textOverflow` | Set overflow characters to display in case the text length is less than the width. See **Text Overflow** below |
### Text Overflow
The `textOverflow` option is used to specify what happens when a text string is too long to fit in the `width` defined.
:information_source: If `textOverflow` is not specified at all, a text label can become wider than the `width` if needed to display the text value.
:information_source: Setting `textOverflow` to an empty string `textOverflow: ""` will cause the item to be truncated if necessary without any characters displayed
:information_source: Otherwise, setting `textOverflow` to one or more characters will truncate the value if necessary and display those characters at the end. i.e. `textOverflow: ...`
## Example
![Example](../../assets/images/text_label_view_example1.png "Text label")
<details>
<summary>Configuration fragment (expand to view)</summary>
<div markdown="1">
```
TL1: {
text: Text label
}
```
</div>
</details>

View File

@ -0,0 +1,83 @@
---
layout: page
title: Toggle Menu View
---
## Toggle Menu View
A toggle menu view supports displaying a list of options on a screen horizontally (side to side, in a single row) similar to a [Horizontal Menu](horizontal_menu_view.md). It is designed to present one of two choices easily.
## General Information
Items can be selected on a menu via the left and right cursor keys, or by selecting them via a `hotKey` - see ***Hot Keys*** below.
:information_source: A toggle menu view is defined with a percent (%) and the characters TM, followed by the view number (if used.) For example: `%TM1`
:information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them.
### Properties
| Property | Description |
|-------------|--------------|
| `textStyle` | Sets the standard (non-focus) text style. See **Text Styles** in [MCI](../mci.md) |
| `focusTextStyle` | Sets focus text style. See **Text Styles** in [MCI](../mci.md)|
| `focus` | If set to `true`, establishes initial focus |
| `submit` | If set to `true` any `accept` action upon this view will submit the encompassing **form** |
| `hotKeys` | Sets hot keys to activate specific items. See **Hot Keys** below |
| `hotKeySubmit` | Set to submit a form on hotkey selection |
| `argName` | Sets the argument name for this selection in the form |
| `items` | List of items to show in the menu. Must include exactly two (2) items. See **Items** below. |
### Hot Keys
A set of `hotKeys` are used to allow the user to press a character on the keyboard to select that item, and optionally submit the form.
Example:
```
hotKeys: { A: 0, B: 1, Q: 1 }
hotKeySubmit: true
```
This would select and submit the first item if `A` is typed, second if `B`, etc.
### Items
A toggle menu, similar to other menus, take a list of items to display in the menu. Unlike other menus, however, there must be exactly two items in a toggle menu. For example:
```
items: [
{
text: First Item
data: first
}
{
text: Second Item
data: second
}
]
```
If the list is for display only (there is no form action associated with it) you can omit the data element, and include the items as a simple list:
```
["First item", "Second item"]
```
## Example
![Example](../../assets/images/toggle_menu_view_example1.gif "Toggle menu")
<details>
<summary>Configuration fragment (expand to view)</summary>
<div markdown="1">
```
TM2: {
focus: true
submit: true
argName: navSelect
focusTextStyle: upper
items: [ "yes", "no" ]
}
```
</div>
</details>

View File

@ -0,0 +1,106 @@
---
layout: page
title: Vertical Menu View
---
## Vertical Menu View
A vertical menu view supports displaying a list of times on a screen vertically in a single column, similar to a lightbar. This type of control is often useful for lists of items or menu controls.
## General Information
Items can be selected on a menu via the cursor keys, Page Up, Page Down, Home, and End, or by selecting them via a `hotKey` - see ***Hot Keys*** below.
:information_source: A vertical menu view is defined with a percent (%) and the characters VM, followed by the view number (if used.) For example: `%VM1`.
:information_source: See [MCI](../mci.md) for general information on how to use views and common configuration properties available for them.
### Properties
| Property | Description |
|-------------|--------------|
| `textStyle` | Sets the standard (non-focus) text style. See **Text Styles** in [MCI](../mci.md) |
| `focusTextStyle` | Sets focus text style. See **Text Styles** in [MCI](../mci.md)|
| `itemSpacing` | Used to separate items vertically in the menu |
| `height` | Sets the height of views to display multiple items vertically (default 1) |
| `focus` | If set to `true`, establishes initial focus |
| `submit` | If set to `true` any `accept` action upon this view will submit the encompassing **form** |
| `hotKeys` | Sets hot keys to activate specific items. See **Hot Keys** below |
| `hotKeySubmit` | Set to submit a form on hotkey selection |
| `argName` | Sets the argument name for this selection in the form |
| `justify` | Sets the justification of each item in the list. Options: left (default), right, center |
| `itemFormat` | Sets the format for a list entry. See **Entry Formatting** in [MCI](../mci.md) |
| `fillChar` | Specifies a character to fill extra space in the menu with. Defaults to an empty space |
| `items` | List of items to show in the menu. See **Items** below.
| `focusItemFormat` | Sets the format for a focused list entry. See **Entry Formatting** in [MCI](../mci.md) |
### Hot Keys
A set of `hotKeys` are used to allow the user to press a character on the keyboard to select that item, and optionally submit the form.
Example:
```
hotKeys: { A: 0, B: 1, C: 2, D: 3 }
hotKeySubmit: true
```
This would select and submit the first item if `A` is typed, second if `B`, etc.
### Items
A vertical menu, similar to other menus, take a list of items to display in the menu. For example:
```
items: [
{
text: First Item
data: first
}
{
text: Second Item
data: second
}
]
```
If the list is for display only (there is no form action associated with it) you can omit the data element, and include the items as a simple list:
```
["First item", "Second item", "Third Item"]
```
## Example
![Example](../../assets/images/vertical_menu_view_example1.gif "Vertical menu")
<details>
<summary>Configuration fragment (expand to view)</summary>
<div markdown="1">
```
VM1: {
submit: true
argName: navSelect
items: [
{
text: login
data: login
}
{
text: apply
data: new user
}
{
text: about
data: about
}
{
text: log off
data: logoff
}
]
}
```
</div>
</details>

View File

@ -6,17 +6,17 @@ All paths mentioned here are relative to the ENiGMA½ checkout directory.
| Directory | Description |
|---------------------|-----------------------------------------------------------------------------------------------------------|
| `/art/general` | Non-theme art - welcome ANSI, logoff ANSI, etc. See [General Art]({{ site.baseurl }}{% link art/general.md %}).
| `/art/themes` | Theme art. Themes should be in their own subdirectory and contain a theme.hjson. See [Themes]({{ site.baseurl }}{% link art/themes.md %}).
| `/art/general` | Non-theme art - welcome ANSI, logoff ANSI, etc. See [General Art](../art/general.md).
| `/art/themes` | Theme art. Themes should be in their own subdirectory and contain a theme.hjson. See [Themes](../art/themes.md).
| `/config` | [config.hjson](config-hjson.md) system configuration.
| `/config/menus` | [menu.hjson](menu-hjson.md) storage.
| `/config/security` | SSL certificates and public/private keys.
| `/db` | All ENiGMA½ databases in SQLite3 format.
| `/docs` | These docs ;-)
| `/dropfiles` | Dropfiles created for [local doors]({{ site.baseurl }}{% link modding/local-doors.md %})
| `/logs` | Logs. See [Monitoring Logs]({{ site.baseurl }}{% link troubleshooting/monitoring-logs.md %})
| `/dropfiles` | Dropfiles created for [local doors](../modding/local-doors.md)
| `/logs` | Logs. See [Monitoring Logs](../troubleshooting/monitoring-logs.md)
| `/misc` | Stuff with no other home; reset password templates, common password lists, other random bits
| `/mods` | User mods. See [Modding]({{ site.baseurl }}{% link modding/existing-mods.md %})
| `/mods` | User mods. See [Modding](../modding/existing-mods.md)
| `/node_modules` | External libraries required by ENiGMA½, installed when you run `npm install`
| `/util` | Various tools used in running/debugging ENiGMA½
| `/www` | ENiGMA½'s built in webserver root directory

View File

@ -3,7 +3,7 @@ layout: page
title: Email
---
## Email Support
ENiGMA½ uses email to send password reset information to users. For it to work, you need to provide valid [Nodemailer](https://nodemailer.com/about/) compatible `email` block in your [config.hjson]({{ site.baseurl }}{% link configuration/config-hjson.md %}). Nodemailer supports SMTP in addition to many pre-defined services for ease of use. The `transport` block within `email` must be Nodemailer compatible.
ENiGMA½ uses email to send password reset information to users. For it to work, you need to provide valid [Nodemailer](https://nodemailer.com/about/) compatible `email` block in your [config.hjson](config-hjson.md). Nodemailer supports SMTP in addition to many pre-defined services for ease of use. The `transport` block within `email` must be Nodemailer compatible.
Additional email support will come in the near future.

View File

@ -36,6 +36,7 @@ File base *Areas* are configured using the `fileBase.areas` configuration block
| `desc` | :-1: | Friendly area description. |
| `storageTags` | :+1: | An array of storage tags for physical storage backing of the files in this area. If uploads are enabled for this area, **first** storage tag location is utilized! |
| `sort` | :-1: | If present, provides the sort key for ordering. `name` is used otherwise. |
| `hashTags` | :-1: | Set to an array of strings or comma separated list to provide _default_ hash tags for this area. |
Example areas section:
@ -45,6 +46,7 @@ areas: {
name: Retro PC
desc: Oldschool PC/DOS
storageTags: [ "retro_pc_dos", "retro_pc_bbs" ]
hashTags: ["retro", "pc", "dos" ]
}
}
```

View File

@ -0,0 +1,72 @@
---
layout: page
title: Docker
---
**You'll need Docker installed before going any further. How to do so are out of scope of these docs, but you can find full instructions
for every operating system on the [Docker website](https://docs.docker.com/engine/install/).**
## Quick Start
prepare a folder where you are going to save your bbs files.
- Generate some config for your BBS: \
you can perform this step from anywhere - but make sure to consistently run it from the same place to retain your config inside the docker guest
```
docker run -it -p 8888:8888 \
--name "ENiGMABBS" \
-v "$(pwd)/config:/enigma-bbs/config" \
-v "$(pwd)/db:/enigma-bbs/db" \
-v "$(pwd)/logs:/enigma-bbs/logs" \
-v "$(pwd)/filebase:/enigma-bbs/filebase" \
-v "$(pwd)/art:/enigma-bbs/art" \
-v "$(pwd)/mods:/enigma-bbs/mods" \
-v "$(pwd)/mail:/mail" \
enigmabbs/enigma-bbs:latest
```
- Run it: \
you can use the same command as above, just daemonize and drop interactiveness (we needed it for config but most of the time docker will run in the background)
````
docker run -d -p 8888:8888 \
--name "ENiGMABBS" \
-v "$(pwd)/config:/enigma-bbs/config" \
-v "$(pwd)/db:/enigma-bbs/db" \
-v "$(pwd)/logs:/enigma-bbs/logs" \
-v "$(pwd)/filebase:/enigma-bbs/filebase" \
-v "$(pwd)/art:/enigma-bbs/art" \
-v "$(pwd)/mods:/enigma-bbs/mods" \
-v "$(pwd)/mail:/mail" \
enigmabbs/enigma-bbs:latest
````
- Restarting and Making changes\
if you make any changes to your host config folder they will persist, and you can just restart ENiGMABBS container to load any changes you've made.
```docker restart ENiGMABBS```
:bulb: Configuration will be stored in `$(pwd)/enigma-bbs/config`.
:bulb: Windows users - you'll need to switch out `$(pwd)/enigma-bbs/config` for a Windows-style path.
## Volumes
Containers by their nature are ephermeral. Meaning, stuff you want to keep (config, database, mail) needs
to be stored outside of the running container. As such, the following volumes are mountable:
| Volume | Usage |
|:------------------------|:---------------------------------------------------------------------|
| /enigma-bbs/art | Art, themes, etc |
| /enigma-bbs/config | Config such as config.hjson, menu.hjson, prompt.hjson, SSL certs etc |
| /enigma-bbs/db | ENiGMA databases |
| /enigma-bbs/filebase | Filebase |
| /enigma-bbs/logs | Logs |
| /enigma-bbs/mods | ENiGMA mods |
| /mail | FTN mail (for use with an external mailer) |
## Building your own image
Customising the Docker image is easy!
1. Clone the ENiGMA-BBS source.
2. Build the image
```
docker build -f ./docker/Dockerfile .
```

View File

@ -6,7 +6,7 @@ For Linux environments it's recommended you run the [install script](install-scr
do things manually, read on...
## Prerequisites
* [Node.js](https://nodejs.org/) version **v12.x LTS or higher** (Other versions may work but are not supported).
* [Node.js](https://nodejs.org/) version **v14.x LTS or higher**. Versions under v14 are known not to work due to language level changes.
* :bulb: It is **highly** recommended to use [Node Version Manager (NVM)](https://github.com/creationix/nvm) to manage your Node.js installation if you're on a Linux/Unix environment.
* [Python](https://www.python.org/downloads/) for compiling Node.js packages with native extensions via `node-gyp`.
@ -57,7 +57,7 @@ ENiGMA BBS makes use of a few packages for archive and legacy protocol support.
:information_source: Additional information in [Archivers](../configuration/archivers.md) and [File Transfer Protocols](../configuration/file-transfer-protocols.md)
## Config Files
You'll need a basic configuration to get started. The main system configuration is handled via `config/config.hjson`. This is an [HJSON](http://hjson.org/) file (compliant JSON is also OK). See [Configuration](../configuration/) for more information.
You'll need a basic configuration to get started. The main system configuration is handled via `config/config.hjson`. This is an [HJSON](http://hjson.org/) file (compliant JSON is also OK). See [Configuration](../configuration/hjson.md) for more information.
Use `oputil.js` to generate your **initial** configuration:

View File

@ -13,7 +13,7 @@ _Note that if you've used the [Docker](docker.md) installation method, you've al
If everything went OK:
```bash
ENiGMA½ Copyright (c) 2014-2020, Bryan Ashby
ENiGMA½ Copyright (c) 2014-2022, Bryan Ashby
_____________________ _____ ____________________ __________\_ /
\__ ____/\_ ____ \ /____/ / _____ __ \ / ______/ // /___jp!
// __|___// | \// |// | \// | | \// \ /___ /_____

Some files were not shown because too many files have changed in this diff Show More