Merge branch '216-waiting-for-caller' of github.com:NuSkooler/enigma-bbs into 216-waiting-for-caller
This commit is contained in:
commit
5288f82006
|
@ -28,5 +28,8 @@
|
|||
],
|
||||
"comma-dangle": 0,
|
||||
"no-trailing-spaces" :"warn"
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2020
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -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.
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
27
UPGRADE.md
27
UPGRADE.md
|
@ -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:
|
||||
|
|
12
WHATSNEW.md
12
WHATSNEW.md
|
@ -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!
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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',
|
||||
|
|
21
core/art.js
21
core/art.js
|
@ -269,19 +269,16 @@ function display(client, art, options, cb) {
|
|||
termHeight : client.term.termHeight,
|
||||
termWidth : client.term.termWidth,
|
||||
trailingLF : options.trailingLF,
|
||||
startRow : options.startRow,
|
||||
});
|
||||
|
||||
let parseComplete = false;
|
||||
let cprListener;
|
||||
let mciMap;
|
||||
const mciCprQueue = [];
|
||||
let artHash;
|
||||
let mciMapFromCache;
|
||||
|
||||
function completed() {
|
||||
if(cprListener) {
|
||||
client.removeListener('cursor position report', cprListener);
|
||||
}
|
||||
|
||||
if(!options.disableMciCache && !mciMapFromCache) {
|
||||
// cache our MCI findings...
|
||||
|
@ -314,18 +311,6 @@ function display(client, art, options, cb) {
|
|||
// no cached MCI info
|
||||
mciMap = {};
|
||||
|
||||
cprListener = function(pos) {
|
||||
if(mciCprQueue.length > 0) {
|
||||
mciMap[mciCprQueue.shift()].position = pos;
|
||||
|
||||
if(parseComplete && 0 === mciCprQueue.length) {
|
||||
return completed();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
client.on('cursor position report', cprListener);
|
||||
|
||||
let generatedId = 100;
|
||||
|
||||
ansiParser.on('mci', mciInfo => {
|
||||
|
@ -339,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());
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -21,7 +21,7 @@ function ButtonView(options) {
|
|||
util.inherits(ButtonView, TextView);
|
||||
|
||||
ButtonView.prototype.onKeyPress = function(ch, key) {
|
||||
if(this.isKeyMapped('accept', key.name) || ' ' === ch) {
|
||||
if(this.isKeyMapped('accept', (key ? key.name : ch)) || ' ' === ch) {
|
||||
this.submitData = 'accept';
|
||||
this.emit('action', 'accept');
|
||||
delete this.submitData;
|
||||
|
@ -29,16 +29,6 @@ ButtonView.prototype.onKeyPress = function(ch, key) {
|
|||
ButtonView.super_.prototype.onKeyPress.call(this, ch, key);
|
||||
}
|
||||
};
|
||||
/*
|
||||
ButtonView.prototype.onKeyPress = function(ch, key) {
|
||||
// allow space = submit
|
||||
if(' ' === ch) {
|
||||
this.emit('action', 'accept');
|
||||
}
|
||||
|
||||
ButtonView.super_.prototype.onKeyPress.call(this, ch, key);
|
||||
};
|
||||
*/
|
||||
|
||||
ButtonView.prototype.getData = function() {
|
||||
return this.submitData || null;
|
||||
|
|
|
@ -4,11 +4,12 @@
|
|||
// ENiGMA½
|
||||
var Log = require('./logger.js').log;
|
||||
var renegadeToAnsi = require('./color_codes.js').renegadeToAnsi;
|
||||
|
||||
const Config = require('./config.js').get;
|
||||
var iconv = require('iconv-lite');
|
||||
var assert = require('assert');
|
||||
var _ = require('lodash');
|
||||
|
||||
|
||||
exports.ClientTerminal = ClientTerminal;
|
||||
|
||||
function ClientTerminal(output) {
|
||||
|
@ -115,7 +116,8 @@ ClientTerminal.prototype.isNixTerm = function() {
|
|||
return true;
|
||||
}
|
||||
|
||||
return [ 'xterm', 'linux', 'screen', 'dumb', 'rxvt', 'konsole', 'gnome', 'x11 terminal emulator' ].includes(this.termType);
|
||||
const utf8TermList = Config().term.utf8TermList;
|
||||
return utf8TermList.includes(this.termType);
|
||||
};
|
||||
|
||||
ClientTerminal.prototype.isANSI = function() {
|
||||
|
@ -153,7 +155,8 @@ ClientTerminal.prototype.isANSI = function() {
|
|||
// linux:
|
||||
// * JuiceSSH (note: TERM=linux also)
|
||||
//
|
||||
return [ 'ansi', 'pcansi', 'pc-ansi', 'ansi-bbs', 'qansi', 'scoansi', 'syncterm', 'ansi-256color' ].includes(this.termType);
|
||||
const cp437TermList = Config().term.cp437TermList;
|
||||
return cp437TermList.includes(this.termType);
|
||||
};
|
||||
|
||||
// :TODO: probably need to update these to convert IAC (0xff) -> IACIAC (escape it)
|
||||
|
|
|
@ -126,7 +126,7 @@ function renegadeToAnsi(s, client) {
|
|||
|
||||
//
|
||||
// Converts various control codes popular in BBS packages
|
||||
// to ANSI escape sequences. Additionaly supports ENiGMA style
|
||||
// to ANSI escape sequences. Additionally supports ENiGMA style
|
||||
// MCI codes.
|
||||
//
|
||||
// Supported control code formats:
|
||||
|
@ -134,16 +134,17 @@ function renegadeToAnsi(s, client) {
|
|||
// * PCBoard : @X## where the first number/char is BG color, and second is FG
|
||||
// * WildCat! : @##@ the same as PCBoard without the X prefix, but with a @ suffix
|
||||
// * WWIV : ^#
|
||||
// * CNET Y-Style : 0x19## where ## is a specific set of codes -- this is the older format
|
||||
// * CNET Q-style : 0x11##} where ## is a specific set of codes -- this is the newer format
|
||||
// * CNET Control-Y: AKA Y-Style -- 0x19## where ## is a specific set of codes (older format)
|
||||
// * CNET Control-Q: AKA Q-style -- 0x11##} where ## is a specific set of codes (newer format)
|
||||
//
|
||||
// TODO: Add Synchronet and Celerity format support
|
||||
//
|
||||
// Resources:
|
||||
// * http://wiki.synchro.net/custom:colors
|
||||
// * https://archive.org/stream/C-Net_Pro_3.0_1994_Perspective_Software/C-Net_Pro_3.0_1994_Perspective_Software_djvu.txt
|
||||
//
|
||||
function controlCodesToAnsi(s, client) {
|
||||
const RE = /(\|([A-Z0-9]{2})|\|)|(@X([0-9A-F]{2}))|(@([0-9A-F]{2})@)|(\x03[0-9]|\x03)|(\x19(c[0-9a-f]|z[0-7]|n1|f1)|\x19)|(\x11(c[0-9a-f]|z[0-7]|n1|f1)}|\x11)/g; // eslint-disable-line no-control-regex
|
||||
const RE = /(\|([A-Z0-9]{2})|\|)|(@X([0-9A-F]{2}))|(@([0-9A-F]{2})@)|(\x03[0-9]|\x03)|(\x19(c[0-9a-f]|z[0-7]|n1|f1|q1)|\x19)|(\x11(c[0-9a-f]|z[0-7]|n1|f1|q1)}|\x11)/g; // eslint-disable-line no-control-regex
|
||||
|
||||
let m;
|
||||
let result = '';
|
||||
|
|
|
@ -17,6 +17,24 @@ module.exports = () => {
|
|||
achievementFile : 'achievements.hjson',
|
||||
},
|
||||
|
||||
term : {
|
||||
// checkUtf8Encoding requires the use of cursor position reports, which are not supported on all terminals.
|
||||
// Using this with a terminal that does not support cursor position reports results in a 2 second delay
|
||||
// during the connect process, but provides better autoconfiguration of utf-8
|
||||
checkUtf8Encoding : true,
|
||||
|
||||
// Checking the ANSI home position also requires the use of cursor position reports, which are not
|
||||
// supported on all terminals. Using this with a terminal that does not support cursor position reports
|
||||
// results in a 3 second delay during the connect process, but works around positioning problems with
|
||||
// non-standard terminals.
|
||||
checkAnsiHomePosition: true,
|
||||
|
||||
// List of terms that should be assumed to use cp437 encoding
|
||||
cp437TermList : ['ansi', 'pcansi', 'pc-ansi', 'ansi-bbs', 'qansi', 'scoansi', 'syncterm', 'ansi-256color', 'ansi-256color-rgb'],
|
||||
// List of terms that should be assumed to use utf8 encoding
|
||||
utf8TermList : ['xterm', 'linux', 'screen', 'dumb', 'rxvt', 'konsole', 'gnome', 'x11 terminal emulator'],
|
||||
},
|
||||
|
||||
users : {
|
||||
usernameMin : 2,
|
||||
usernameMax : 16, // Note that FidoNet wants 36 max
|
||||
|
@ -166,10 +184,11 @@ module.exports = () => {
|
|||
'ecdh-sha2-nistp256',
|
||||
'ecdh-sha2-nistp384',
|
||||
'ecdh-sha2-nistp521',
|
||||
'diffie-hellman-group-exchange-sha256',
|
||||
'diffie-hellman-group14-sha1',
|
||||
'diffie-hellman-group-exchange-sha1',
|
||||
'diffie-hellman-group1-sha1',
|
||||
// Group exchange not currnetly supported
|
||||
// 'diffie-hellman-group-exchange-sha256',
|
||||
// 'diffie-hellman-group-exchange-sha1',
|
||||
],
|
||||
cipher : [
|
||||
'aes128-ctr',
|
||||
|
@ -492,7 +511,7 @@ module.exports = () => {
|
|||
},
|
||||
decompress : {
|
||||
cmd : '7za',
|
||||
args : [ 'e', '-o{extractPath}', '{archivePath}' ] // :TODO: should be 'x'?
|
||||
args : [ 'e', '-y', '-o{extractPath}', '{archivePath}' ] // :TODO: should be 'x'?
|
||||
},
|
||||
list : {
|
||||
cmd : '7za',
|
||||
|
@ -501,7 +520,7 @@ module.exports = () => {
|
|||
},
|
||||
extract : {
|
||||
cmd : '7za',
|
||||
args : [ 'e', '-o{extractPath}', '{archivePath}', '{fileList}' ],
|
||||
args : [ 'e', '-y', '-o{extractPath}', '{archivePath}', '{fileList}' ],
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
// ENiGMA½
|
||||
const ansi = require('./ansi_term.js');
|
||||
const Events = require('./events.js');
|
||||
const Config = require('./config.js').get;
|
||||
const { Errors } = require('./enig_error.js');
|
||||
|
||||
// deps
|
||||
|
@ -18,6 +19,13 @@ function ansiDiscoverHomePosition(client, cb) {
|
|||
// think of home as 0,0. If this is the case, we need to offset
|
||||
// our positioning to accommodate for such.
|
||||
//
|
||||
|
||||
|
||||
if( !Config().term.checkAnsiHomePosition ) {
|
||||
// Skip (and assume 1,1) if the home position check is disabled.
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
const done = (err) => {
|
||||
client.removeListener('cursor position report', cprListener);
|
||||
clearTimeout(giveUpTimer);
|
||||
|
@ -68,8 +76,9 @@ function ansiAttemptDetectUTF8(client, cb) {
|
|||
//
|
||||
// We currently only do this if the term hasn't already been ID'd as a
|
||||
// "*nix" terminal -- that is, xterm, etc.
|
||||
//
|
||||
if(!client.term.isNixTerm()) {
|
||||
// Also skip this check if checkUtf8Encoding is disabled in the config
|
||||
|
||||
if(!client.term.isNixTerm() || !Config().term.checkUtf8Encoding) {
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
|
@ -119,6 +128,8 @@ function ansiAttemptDetectUTF8(client, cb) {
|
|||
return giveUp();
|
||||
}, 2000);
|
||||
|
||||
|
||||
|
||||
client.once('cursor position report', cprListener);
|
||||
client.term.rawWrite(ansi.goHome() + ansi.queryPos());
|
||||
}
|
||||
|
@ -199,7 +210,7 @@ function displayBanner(term) {
|
|||
// note: intentional formatting:
|
||||
term.pipeWrite(`
|
||||
|06Connected to |02EN|10i|02GMA|10½ |06BBS version |12|VN
|
||||
|06Copyright (c) 2014-2020 Bryan Ashby |14- |12http://l33t.codes/
|
||||
|06Copyright (c) 2014-2022 Bryan Ashby |14- |12http://l33t.codes/
|
||||
|06Updates & source |14- |12https://github.com/NuSkooler/enigma-bbs/
|
||||
|00`
|
||||
);
|
||||
|
@ -216,7 +227,6 @@ function connectEntry(client, nextMenu) {
|
|||
},
|
||||
function discoverHomePosition(callback) {
|
||||
ansiDiscoverHomePosition(client, () => {
|
||||
// :TODO: If CPR for home fully fails, we should bail out on the connection with an error, e.g. ANSI support required
|
||||
return callback(null); // we try to continue anyway
|
||||
});
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
//
|
||||
|
|
131
core/fse.js
131
core/fse.js
|
@ -21,7 +21,10 @@ const {
|
|||
isAnsi, stripAnsiControlCodes,
|
||||
insert
|
||||
} = require('./string_util.js');
|
||||
const { stripMciColorCodes } = require('./color_codes.js');
|
||||
const {
|
||||
stripMciColorCodes,
|
||||
controlCodesToAnsi,
|
||||
} = require('./color_codes.js');
|
||||
const Config = require('./config.js').get;
|
||||
const { getAddressedToInfo } = require('./mail_util.js');
|
||||
const Events = require('./events.js');
|
||||
|
@ -418,7 +421,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
|
|||
//
|
||||
// Find tearline - we want to color it differently.
|
||||
//
|
||||
const tearLinePos = this.message.getTearLinePosition(msg);
|
||||
const tearLinePos = Message.getTearLinePosition(msg);
|
||||
|
||||
if(tearLinePos > -1) {
|
||||
msg = insert(msg, tearLinePos, bodyMessageView.getSGRFor('text'));
|
||||
|
@ -432,7 +435,55 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
|
|||
}
|
||||
);
|
||||
} else {
|
||||
bodyMessageView.setText(stripAnsiControlCodes(msg));
|
||||
msg = stripAnsiControlCodes(msg); // start clean
|
||||
|
||||
const styleToArray = (style, len) => {
|
||||
if (!Array.isArray(style)) {
|
||||
style = [ style ];
|
||||
}
|
||||
while (style.length < len) {
|
||||
style.push(style[0]);
|
||||
}
|
||||
return style;
|
||||
};
|
||||
|
||||
//
|
||||
// In *View* mode, if enabled, do a little prep work so we can stylize:
|
||||
// - Quote indicators
|
||||
// - Tear lines
|
||||
// - Origins
|
||||
//
|
||||
if (this.menuConfig.config.quoteStyleLevel1) {
|
||||
// can be a single style to cover 'XX> TEXT' or an array to cover 'XX', '>', and TEXT
|
||||
// Non-standard (as for BBSes) single > TEXT, omitting space before XX, etc. are allowed
|
||||
const styleL1 = styleToArray(this.menuConfig.config.quoteStyleLevel1, 3);
|
||||
|
||||
const QuoteRegex = /^([ ]?)([!-~]{0,2})>([ ]*)([^\r\n]*\r?\n)/gm;
|
||||
msg = msg.replace(QuoteRegex, (m, spc1, initials, spc2, text) => {
|
||||
return `${spc1}${styleL1[0]}${initials}${styleL1[1]}>${spc2}${styleL1[2]}${text}${bodyMessageView.styleSGR1}`;
|
||||
});
|
||||
}
|
||||
|
||||
if (this.menuConfig.config.tearLineStyle) {
|
||||
// '---' and TEXT
|
||||
const style = styleToArray(this.menuConfig.config.tearLineStyle, 2);
|
||||
|
||||
const TearLineRegex = /^--- (.+)$(?![\s\S]*^--- .+$)/m;
|
||||
msg = msg.replace(TearLineRegex, (m, text) => {
|
||||
return `${style[0]}--- ${style[1]}${text}${bodyMessageView.styleSGR1}`;
|
||||
});
|
||||
}
|
||||
|
||||
if (this.menuConfig.config.originStyle) {
|
||||
const style = styleToArray(this.menuConfig.config.originStyle, 3);
|
||||
|
||||
const OriginRegex = /^([ ]{1,2})\* Origin: (.+)$/m;
|
||||
msg = msg.replace(OriginRegex, (m, spc, text) => {
|
||||
return `${spc}${style[0]}* ${style[1]}Origin: ${style[2]}${text}${bodyMessageView.styleSGR1}`;
|
||||
});
|
||||
}
|
||||
|
||||
bodyMessageView.setText(controlCodesToAnsi(msg));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -552,7 +603,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
|
|||
theme.displayThemedAsset(
|
||||
footerArt,
|
||||
self.client,
|
||||
{ font : self.menuConfig.font },
|
||||
{ font : self.menuConfig.font, startRow: self.header.height + self.body.height },
|
||||
function displayed(err, artData) {
|
||||
callback(err, artData);
|
||||
}
|
||||
|
@ -575,19 +626,34 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
|
|||
async.series(
|
||||
[
|
||||
function displayHeaderAndBody(callback) {
|
||||
async.eachSeries( comps, function dispArt(n, next) {
|
||||
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);
|
||||
|
||||
|
|
|
@ -0,0 +1,511 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const MenuView = require('./menu_view.js').MenuView;
|
||||
const ansi = require('./ansi_term.js');
|
||||
const strUtil = require('./string_util.js');
|
||||
const formatString = require('./string_format');
|
||||
const pipeToAnsi = require('./color_codes.js').pipeToAnsi;
|
||||
|
||||
// deps
|
||||
const util = require('util');
|
||||
const _ = require('lodash');
|
||||
|
||||
exports.FullMenuView = FullMenuView;
|
||||
|
||||
function FullMenuView(options) {
|
||||
options.cursor = options.cursor || 'hide';
|
||||
options.justify = options.justify || 'left';
|
||||
|
||||
|
||||
MenuView.call(this, options);
|
||||
|
||||
|
||||
// Initialize paging
|
||||
this.pages = [];
|
||||
this.currentPage = 0;
|
||||
|
||||
this.initDefaultWidth();
|
||||
|
||||
// we want page up/page down by default
|
||||
if (!_.isObject(options.specialKeyMap)) {
|
||||
Object.assign(this.specialKeyMap, {
|
||||
'page up': ['page up'],
|
||||
'page down': ['page down'],
|
||||
});
|
||||
}
|
||||
|
||||
this.autoAdjustHeightIfEnabled = () => {
|
||||
if (this.autoAdjustHeight) {
|
||||
this.dimens.height = (this.items.length * (this.itemSpacing + 1)) - (this.itemSpacing);
|
||||
this.dimens.height = Math.min(this.dimens.height, this.client.term.termHeight - this.position.row);
|
||||
}
|
||||
|
||||
this.positionCacheExpired = true;
|
||||
};
|
||||
|
||||
this.autoAdjustHeightIfEnabled();
|
||||
|
||||
this.clearPage = () => {
|
||||
let width = this.dimens.width;
|
||||
if (this.oldDimens) {
|
||||
if (this.oldDimens.width > width) {
|
||||
width = this.oldDimens.width;
|
||||
}
|
||||
delete this.oldDimens;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.dimens.height; i++) {
|
||||
const text = `${strUtil.pad(this.fillChar, width, this.fillChar, 'left')}`;
|
||||
this.client.term.write(`${ansi.goto(this.position.row + i, this.position.col)}${this.getSGR()}${text}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.cachePositions = () => {
|
||||
if (this.positionCacheExpired) {
|
||||
// first, clear the page
|
||||
this.clearPage();
|
||||
|
||||
|
||||
this.autoAdjustHeightIfEnabled();
|
||||
|
||||
this.pages = []; // reset
|
||||
|
||||
// Calculate number of items visible per column
|
||||
this.itemsPerRow = Math.floor(this.dimens.height / (this.itemSpacing + 1));
|
||||
// handle case where one can fit at the end
|
||||
if (this.dimens.height > (this.itemsPerRow * (this.itemSpacing + 1))) {
|
||||
this.itemsPerRow++;
|
||||
}
|
||||
|
||||
// Final check to make sure we don't try to display more than we have
|
||||
if (this.itemsPerRow > this.items.length) {
|
||||
this.itemsPerRow = this.items.length;
|
||||
}
|
||||
|
||||
let col = this.position.col;
|
||||
let row = this.position.row;
|
||||
const spacer = new Array(this.itemHorizSpacing + 1).join(this.fillChar);
|
||||
|
||||
let itemInRow = 0;
|
||||
let itemInCol = 0;
|
||||
|
||||
let pageStart = 0;
|
||||
|
||||
for (let i = 0; i < this.items.length; ++i) {
|
||||
itemInRow++;
|
||||
this.items[i].row = row;
|
||||
this.items[i].col = col;
|
||||
this.items[i].itemInRow = itemInRow;
|
||||
|
||||
row += this.itemSpacing + 1;
|
||||
|
||||
// have to calculate the max length on the last entry
|
||||
if (i == this.items.length - 1) {
|
||||
let maxLength = 0;
|
||||
for (let j = 0; j < this.itemsPerRow; j++) {
|
||||
if (this.items[i - j].col != this.items[i].col) {
|
||||
break;
|
||||
}
|
||||
const itemLength = this.items[i - j].text.length;
|
||||
if (itemLength > maxLength) {
|
||||
maxLength = itemLength;
|
||||
}
|
||||
}
|
||||
|
||||
// set length on each item in the column
|
||||
for (let j = 0; j < this.itemsPerRow; j++) {
|
||||
if (this.items[i - j].col != this.items[i].col) {
|
||||
break;
|
||||
}
|
||||
this.items[i - j].fixedLength = maxLength;
|
||||
}
|
||||
|
||||
|
||||
// Check if we have room for this column
|
||||
// skip for column 0, we need at least one
|
||||
if (itemInCol != 0 && (col + maxLength > this.dimens.width)) {
|
||||
// save previous page
|
||||
this.pages.push({ start: pageStart, end: i - itemInRow });
|
||||
|
||||
// fix the last column processed
|
||||
for (let j = 0; j < this.itemsPerRow; j++) {
|
||||
if (this.items[i - j].col != col) {
|
||||
break;
|
||||
}
|
||||
this.items[i - j].col = this.position.col;
|
||||
pageStart = i - j;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Since this is the last page, save the current page as well
|
||||
this.pages.push({ start: pageStart, end: i });
|
||||
|
||||
}
|
||||
// also handle going to next column
|
||||
else if (itemInRow == this.itemsPerRow) {
|
||||
itemInRow = 0;
|
||||
|
||||
// restart row for next column
|
||||
row = this.position.row;
|
||||
let maxLength = 0;
|
||||
for (let j = 0; j < this.itemsPerRow; j++) {
|
||||
// TODO: handle complex items
|
||||
let itemLength = this.items[i - j].text.length;
|
||||
if (itemLength > maxLength) {
|
||||
maxLength = itemLength;
|
||||
}
|
||||
}
|
||||
|
||||
// set length on each item in the column
|
||||
for (let j = 0; j < this.itemsPerRow; j++) {
|
||||
this.items[i - j].fixedLength = maxLength;
|
||||
}
|
||||
|
||||
// Check if we have room for this column in the current page
|
||||
// skip for first column, we need at least one
|
||||
if (itemInCol != 0 && (col + maxLength > this.dimens.width)) {
|
||||
// save previous page
|
||||
this.pages.push({ start: pageStart, end: i - this.itemsPerRow });
|
||||
|
||||
// restart page start for next page
|
||||
pageStart = i - this.itemsPerRow + 1;
|
||||
|
||||
// reset
|
||||
col = this.position.col;
|
||||
itemInRow = 0;
|
||||
|
||||
// fix the last column processed
|
||||
for (let j = 0; j < this.itemsPerRow; j++) {
|
||||
this.items[i - j].col = col;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// increment the column
|
||||
col += maxLength + spacer.length;
|
||||
itemInCol++;
|
||||
}
|
||||
|
||||
|
||||
// Set the current page if the current item is focused.
|
||||
if (this.focusedItemIndex === i) {
|
||||
this.currentPage = this.pages.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.positionCacheExpired = false;
|
||||
};
|
||||
|
||||
this.drawItem = (index) => {
|
||||
const item = this.items[index];
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cached = this.getRenderCacheItem(index, item.focused);
|
||||
if (cached) {
|
||||
return this.client.term.write(`${ansi.goto(item.row, item.col)}${cached}`);
|
||||
}
|
||||
|
||||
let text;
|
||||
let sgr;
|
||||
if (item.focused && this.hasFocusItems()) {
|
||||
const focusItem = this.focusItems[index];
|
||||
text = focusItem ? focusItem.text : item.text;
|
||||
sgr = '';
|
||||
} else if (this.complexItems) {
|
||||
text = pipeToAnsi(formatString(item.focused && this.focusItemFormat ? this.focusItemFormat : this.itemFormat, item));
|
||||
sgr = this.focusItemFormat ? '' : (index === this.focusedItemIndex ? this.getFocusSGR() : this.getSGR());
|
||||
} else {
|
||||
text = strUtil.stylizeString(item.text, item.focused ? this.focusTextStyle : this.textStyle);
|
||||
sgr = (index === this.focusedItemIndex ? this.getFocusSGR() : this.getSGR());
|
||||
}
|
||||
|
||||
let renderLength = strUtil.renderStringLength(text);
|
||||
if (this.hasTextOverflow() && (item.col + renderLength) > this.dimens.width) {
|
||||
text = strUtil.renderSubstr(text, 0, this.dimens.width - (item.col + this.textOverflow.length)) + this.textOverflow;
|
||||
}
|
||||
|
||||
let padLength = Math.min(item.fixedLength + 1, this.dimens.width);
|
||||
|
||||
text = `${sgr}${strUtil.pad(text, padLength, this.fillChar, this.justify)}${this.getSGR()}`;
|
||||
this.client.term.write(`${ansi.goto(item.row, item.col)}${text}`);
|
||||
this.setRenderCacheItem(index, text, item.focused);
|
||||
};
|
||||
}
|
||||
|
||||
util.inherits(FullMenuView, MenuView);
|
||||
|
||||
FullMenuView.prototype.redraw = function() {
|
||||
FullMenuView.super_.prototype.redraw.call(this);
|
||||
|
||||
this.cachePositions();
|
||||
|
||||
if (this.items.length) {
|
||||
for (let i = this.pages[this.currentPage].start; i <= this.pages[this.currentPage].end; ++i) {
|
||||
this.items[i].focused = this.focusedItemIndex === i;
|
||||
this.drawItem(i);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
FullMenuView.prototype.setHeight = function(height) {
|
||||
this.oldDimens = Object.assign({}, this.dimens);
|
||||
|
||||
FullMenuView.super_.prototype.setHeight.call(this, height);
|
||||
|
||||
this.positionCacheExpired = true;
|
||||
this.autoAdjustHeight = false;
|
||||
};
|
||||
|
||||
FullMenuView.prototype.setWidth = function(width) {
|
||||
this.oldDimens = Object.assign({}, this.dimens);
|
||||
|
||||
FullMenuView.super_.prototype.setWidth.call(this, width);
|
||||
|
||||
this.positionCacheExpired = true;
|
||||
};
|
||||
|
||||
FullMenuView.prototype.setTextOverflow = function(overflow) {
|
||||
FullMenuView.super_.prototype.setTextOverflow.call(this, overflow);
|
||||
|
||||
this.positionCacheExpired = true;
|
||||
|
||||
}
|
||||
|
||||
FullMenuView.prototype.setPosition = function(pos) {
|
||||
FullMenuView.super_.prototype.setPosition.call(this, pos);
|
||||
|
||||
this.positionCacheExpired = true;
|
||||
};
|
||||
|
||||
FullMenuView.prototype.setFocus = function(focused) {
|
||||
FullMenuView.super_.prototype.setFocus.call(this, focused);
|
||||
this.positionCacheExpired = true;
|
||||
this.autoAdjustHeight = false;
|
||||
|
||||
this.redraw();
|
||||
};
|
||||
|
||||
FullMenuView.prototype.setFocusItemIndex = function(index) {
|
||||
FullMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex
|
||||
};
|
||||
|
||||
FullMenuView.prototype.onKeyPress = function(ch, key) {
|
||||
if (key) {
|
||||
if (this.isKeyMapped('up', key.name)) {
|
||||
this.focusPrevious();
|
||||
} else if (this.isKeyMapped('down', key.name)) {
|
||||
this.focusNext();
|
||||
} else if (this.isKeyMapped('left', key.name)) {
|
||||
this.focusPreviousColumn();
|
||||
} else if (this.isKeyMapped('right', key.name)) {
|
||||
this.focusNextColumn();
|
||||
} else if (this.isKeyMapped('page up', key.name)) {
|
||||
this.focusPreviousPageItem();
|
||||
} else if (this.isKeyMapped('page down', key.name)) {
|
||||
this.focusNextPageItem();
|
||||
} else if (this.isKeyMapped('home', key.name)) {
|
||||
this.focusFirst();
|
||||
} else if (this.isKeyMapped('end', key.name)) {
|
||||
this.focusLast();
|
||||
}
|
||||
}
|
||||
|
||||
FullMenuView.super_.prototype.onKeyPress.call(this, ch, key);
|
||||
};
|
||||
|
||||
FullMenuView.prototype.getData = function() {
|
||||
const item = this.getItem(this.focusedItemIndex);
|
||||
return _.isString(item.data) ? item.data : this.focusedItemIndex;
|
||||
};
|
||||
|
||||
FullMenuView.prototype.setItems = function(items) {
|
||||
// if we have items already, save off their drawing area so we don't leave fragments at redraw
|
||||
if (this.items && this.items.length) {
|
||||
this.oldDimens = Object.assign({}, this.dimens);
|
||||
}
|
||||
|
||||
FullMenuView.super_.prototype.setItems.call(this, items);
|
||||
|
||||
this.positionCacheExpired = true;
|
||||
};
|
||||
|
||||
FullMenuView.prototype.removeItem = function(index) {
|
||||
if (this.items && this.items.length) {
|
||||
this.oldDimens = Object.assign({}, this.dimens);
|
||||
}
|
||||
|
||||
FullMenuView.super_.prototype.removeItem.call(this, index);
|
||||
this.positionCacheExpired = true;
|
||||
};
|
||||
|
||||
FullMenuView.prototype.focusNext = function() {
|
||||
if (this.items.length - 1 === this.focusedItemIndex) {
|
||||
this.clearPage();
|
||||
this.focusedItemIndex = 0;
|
||||
this.currentPage = 0;
|
||||
}
|
||||
else {
|
||||
this.focusedItemIndex++;
|
||||
if (this.focusedItemIndex > this.pages[this.currentPage].end) {
|
||||
this.clearPage();
|
||||
this.currentPage++;
|
||||
}
|
||||
}
|
||||
|
||||
this.redraw();
|
||||
|
||||
FullMenuView.super_.prototype.focusNext.call(this);
|
||||
};
|
||||
|
||||
FullMenuView.prototype.focusPrevious = function() {
|
||||
if (0 === this.focusedItemIndex) {
|
||||
this.clearPage();
|
||||
this.focusedItemIndex = this.items.length - 1;
|
||||
this.currentPage = this.pages.length - 1;
|
||||
}
|
||||
else {
|
||||
this.focusedItemIndex--;
|
||||
if (this.focusedItemIndex < this.pages[this.currentPage].start) {
|
||||
this.clearPage();
|
||||
this.currentPage--;
|
||||
}
|
||||
}
|
||||
|
||||
this.redraw();
|
||||
|
||||
FullMenuView.super_.prototype.focusPrevious.call(this);
|
||||
};
|
||||
|
||||
FullMenuView.prototype.focusPreviousColumn = function() {
|
||||
|
||||
const currentRow = this.items[this.focusedItemIndex].itemInRow;
|
||||
this.focusedItemIndex = this.focusedItemIndex - this.itemsPerRow;
|
||||
if (this.focusedItemIndex < 0) {
|
||||
this.clearPage();
|
||||
const lastItemRow = this.items[this.items.length - 1].itemInRow;
|
||||
if (lastItemRow > currentRow) {
|
||||
this.focusedItemIndex = this.items.length - (lastItemRow - currentRow) - 1;
|
||||
}
|
||||
else {
|
||||
// can't go to same column, so go to last item
|
||||
this.focusedItemIndex = this.items.length - 1;
|
||||
}
|
||||
// set to last page
|
||||
this.currentPage = this.pages.length - 1;
|
||||
}
|
||||
else {
|
||||
if (this.focusedItemIndex < this.pages[this.currentPage].start) {
|
||||
this.clearPage();
|
||||
this.currentPage--;
|
||||
}
|
||||
}
|
||||
|
||||
this.redraw();
|
||||
|
||||
// TODO: This isn't specific to Previous, may want to replace in the future
|
||||
FullMenuView.super_.prototype.focusPrevious.call(this);
|
||||
};
|
||||
|
||||
FullMenuView.prototype.focusNextColumn = function() {
|
||||
|
||||
const currentRow = this.items[this.focusedItemIndex].itemInRow;
|
||||
this.focusedItemIndex = this.focusedItemIndex + this.itemsPerRow;
|
||||
if (this.focusedItemIndex > this.items.length - 1) {
|
||||
this.focusedItemIndex = currentRow - 1;
|
||||
this.currentPage = 0;
|
||||
this.clearPage();
|
||||
}
|
||||
else if (this.focusedItemIndex > this.pages[this.currentPage].end) {
|
||||
this.clearPage();
|
||||
this.currentPage++;
|
||||
}
|
||||
|
||||
this.redraw();
|
||||
|
||||
// TODO: This isn't specific to Next, may want to replace in the future
|
||||
FullMenuView.super_.prototype.focusNext.call(this);
|
||||
};
|
||||
|
||||
FullMenuView.prototype.focusPreviousPageItem = function() {
|
||||
|
||||
// handle first page
|
||||
if (this.currentPage == 0) {
|
||||
// Do nothing, page up shouldn't go down on last page
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentPage--;
|
||||
this.focusedItemIndex = this.pages[this.currentPage].start;
|
||||
this.clearPage();
|
||||
|
||||
this.redraw();
|
||||
|
||||
return FullMenuView.super_.prototype.focusPreviousPageItem.call(this);
|
||||
};
|
||||
|
||||
FullMenuView.prototype.focusNextPageItem = function() {
|
||||
|
||||
// handle last page
|
||||
if (this.currentPage == this.pages.length - 1) {
|
||||
// Do nothing, page up shouldn't go down on last page
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentPage++;
|
||||
this.focusedItemIndex = this.pages[this.currentPage].start;
|
||||
this.clearPage();
|
||||
|
||||
this.redraw();
|
||||
|
||||
return FullMenuView.super_.prototype.focusNextPageItem.call(this);
|
||||
};
|
||||
|
||||
FullMenuView.prototype.focusFirst = function() {
|
||||
|
||||
this.currentPage = 0;
|
||||
this.focusedItemIndex = 0;
|
||||
this.clearPage();
|
||||
|
||||
this.redraw();
|
||||
return FullMenuView.super_.prototype.focusFirst.call(this);
|
||||
};
|
||||
|
||||
FullMenuView.prototype.focusLast = function() {
|
||||
|
||||
this.currentPage = this.pages.length - 1;
|
||||
this.focusedItemIndex = this.pages[this.currentPage].end;
|
||||
this.clearPage();
|
||||
|
||||
this.redraw();
|
||||
return FullMenuView.super_.prototype.focusLast.call(this);
|
||||
};
|
||||
|
||||
FullMenuView.prototype.setFocusItems = function(items) {
|
||||
FullMenuView.super_.prototype.setFocusItems.call(this, items);
|
||||
|
||||
this.positionCacheExpired = true;
|
||||
};
|
||||
|
||||
FullMenuView.prototype.setItemSpacing = function(itemSpacing) {
|
||||
FullMenuView.super_.prototype.setItemSpacing.call(this, itemSpacing);
|
||||
|
||||
this.positionCacheExpired = true;
|
||||
};
|
||||
|
||||
FullMenuView.prototype.setJustify = function(justify) {
|
||||
FullMenuView.super_.prototype.setJustify.call(this, justify);
|
||||
this.positionCacheExpired = true;
|
||||
};
|
||||
|
||||
|
||||
FullMenuView.prototype.setItemHorizSpacing = function(itemHorizSpacing) {
|
||||
FullMenuView.super_.prototype.setItemHorizSpacing.call(this, itemHorizSpacing);
|
||||
|
||||
this.positionCacheExpired = true;
|
||||
};
|
|
@ -132,7 +132,7 @@ MaskEditTextView.prototype.onKeyPress = function(ch, key) {
|
|||
this.text = this.text.substr(0, this.text.length - 1);
|
||||
this.clientBackspace();
|
||||
} else {
|
||||
while(this.patternArrayPos > 0) {
|
||||
while(this.patternArrayPos >= 0) {
|
||||
if(_.isRegExp(this.patternArray[this.patternArrayPos])) {
|
||||
this.text = this.text.substr(0, this.text.length - 1);
|
||||
this.client.term.write(ansi.goto(this.position.row, this.getEndOfTextColumn() + 1));
|
||||
|
|
|
@ -8,6 +8,7 @@ const EditTextView = require('./edit_text_view.js').EditTextView;
|
|||
const ButtonView = require('./button_view.js').ButtonView;
|
||||
const VerticalMenuView = require('./vertical_menu_view.js').VerticalMenuView;
|
||||
const HorizontalMenuView = require('./horizontal_menu_view.js').HorizontalMenuView;
|
||||
const FullMenuView = require('./full_menu_view.js').FullMenuView;
|
||||
const SpinnerMenuView = require('./spinner_menu_view.js').SpinnerMenuView;
|
||||
const ToggleMenuView = require('./toggle_menu_view.js').ToggleMenuView;
|
||||
const MaskEditTextView = require('./mask_edit_text_view.js').MaskEditTextView;
|
||||
|
@ -27,7 +28,7 @@ function MCIViewFactory(client) {
|
|||
}
|
||||
|
||||
MCIViewFactory.UserViewCodes = [
|
||||
'TL', 'ET', 'ME', 'MT', 'PL', 'BT', 'VM', 'HM', 'SM', 'TM', 'KE',
|
||||
'TL', 'ET', 'ME', 'MT', 'PL', 'BT', 'VM', 'HM', 'FM', 'SM', 'TM', 'KE',
|
||||
|
||||
//
|
||||
// XY is a special MCI code that allows finding positions
|
||||
|
@ -164,6 +165,18 @@ MCIViewFactory.prototype.createFromMCI = function(mci) {
|
|||
view = new HorizontalMenuView(options);
|
||||
break;
|
||||
|
||||
// Full Menu
|
||||
case 'FM' :
|
||||
setOption(0, 'itemSpacing');
|
||||
setOption(1, 'itemHorizSpacing');
|
||||
setOption(2, 'justify');
|
||||
setOption(3, 'textStyle');
|
||||
|
||||
setFocusOption(0, 'focusTextStyle');
|
||||
|
||||
view = new FullMenuView(options);
|
||||
break;
|
||||
|
||||
case 'SM' :
|
||||
setOption(0, 'textStyle');
|
||||
setOption(1, 'justify');
|
||||
|
|
|
@ -56,14 +56,14 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||
initSequence() {
|
||||
const self = this;
|
||||
const mciData = {};
|
||||
let pausePosition;
|
||||
let pausePosition = {row: 0, column: 0};
|
||||
|
||||
const hasArt = () => {
|
||||
return _.isString(self.menuConfig.art) ||
|
||||
(Array.isArray(self.menuConfig.art) && _.has(self.menuConfig.art[0], 'acs'));
|
||||
};
|
||||
|
||||
async.series(
|
||||
async.waterfall(
|
||||
[
|
||||
function beforeArtInterrupt(callback) {
|
||||
return self.displayQueuedInterruptions(callback);
|
||||
|
@ -73,7 +73,7 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||
},
|
||||
function displayMenuArt(callback) {
|
||||
if(!hasArt()) {
|
||||
return callback(null);
|
||||
return callback(null, null);
|
||||
}
|
||||
|
||||
self.displayAsset(
|
||||
|
@ -86,18 +86,15 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||
mciData.menu = artData.mciMap;
|
||||
}
|
||||
|
||||
return callback(null); // any errors are non-fatal
|
||||
if(artData) {
|
||||
pausePosition.row = artData.height + 1;
|
||||
}
|
||||
|
||||
return callback(null, artData); // any errors are non-fatal
|
||||
}
|
||||
);
|
||||
},
|
||||
function moveToPromptLocation(callback) {
|
||||
if(self.menuConfig.prompt) {
|
||||
// :TODO: fetch and move cursor to prompt location, if supplied. See notes/etc. on placements
|
||||
}
|
||||
|
||||
return callback(null);
|
||||
},
|
||||
function displayPromptArt(callback) {
|
||||
function displayPromptArt(artData, callback) {
|
||||
if(!_.isString(self.menuConfig.prompt)) {
|
||||
return callback(null);
|
||||
}
|
||||
|
@ -106,41 +103,41 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||
return callback(Errors.MissingConfig('Prompt specified but no "promptConfig" block found'));
|
||||
}
|
||||
|
||||
const options = Object.assign({}, self.menuConfig.config);
|
||||
|
||||
if(_.isNumber(artData?.height)) {
|
||||
options.startRow = artData.height + 1;
|
||||
}
|
||||
|
||||
self.displayAsset(
|
||||
self.menuConfig.promptConfig.art,
|
||||
self.menuConfig.config,
|
||||
options,
|
||||
(err, artData) => {
|
||||
if(artData) {
|
||||
mciData.prompt = artData.mciMap;
|
||||
pausePosition.row = artData.height + 1;
|
||||
}
|
||||
|
||||
return callback(err); // pass err here; prompts *must* have art
|
||||
}
|
||||
);
|
||||
},
|
||||
function recordCursorPosition(callback) {
|
||||
if(!self.shouldPause()) {
|
||||
return callback(null); // cursor position not needed
|
||||
}
|
||||
|
||||
self.client.once('cursor position report', pos => {
|
||||
pausePosition = { row : pos[0], col : 1 };
|
||||
self.client.log.trace('After art position recorded', pausePosition );
|
||||
return callback(null);
|
||||
});
|
||||
|
||||
self.client.term.rawWrite(ansi.queryPos());
|
||||
},
|
||||
function afterArtDisplayed(callback) {
|
||||
return self.mciReady(mciData, callback);
|
||||
},
|
||||
function displayPauseIfRequested(callback) {
|
||||
if(!self.shouldPause()) {
|
||||
return callback(null);
|
||||
return callback(null, null);
|
||||
}
|
||||
|
||||
if(self.client.term.termHeight > 0 && pausePosition.row > self.client.termHeight) {
|
||||
// If this scrolled, the prompt will go to the bottom of the screen
|
||||
pausePosition.row = self.client.termHeight;
|
||||
}
|
||||
|
||||
return self.pausePrompt(pausePosition, callback);
|
||||
},
|
||||
function finishAndNext(callback) {
|
||||
function finishAndNext(artInfo, callback) {
|
||||
self.finishedLoading();
|
||||
self.realTimeInterrupt = 'allowed';
|
||||
return self.autoNextMenu(callback);
|
||||
|
@ -512,7 +509,7 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
|||
|
||||
this.optionalMoveToPosition(position);
|
||||
|
||||
return theme.displayThemedPause(this.client, cb);
|
||||
return theme.displayThemedPause(this.client, {position}, cb);
|
||||
}
|
||||
|
||||
promptForInput( { formName, formId, promptName, prevFormName, position } = {}, options, cb) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -790,7 +790,7 @@ module.exports = class Message {
|
|||
return ftnUtil.getQuotePrefix(this[source]);
|
||||
}
|
||||
|
||||
getTearLinePosition(input) {
|
||||
static getTearLinePosition(input) {
|
||||
const m = input.match(/^--- .+$(?![\s\S]*^--- .+$)/m);
|
||||
return m ? m.index : -1;
|
||||
}
|
||||
|
@ -886,12 +886,12 @@ module.exports = class Message {
|
|||
}
|
||||
);
|
||||
} else {
|
||||
const QUOTE_RE = /^ ((?:[A-Za-z0-9]{2}> )+(?:[A-Za-z0-9]{2}>)*) */;
|
||||
const QUOTE_RE = /^ ((?:[A-Za-z0-9]{1,2}> )+(?:[A-Za-z0-9]{1,2}>)*) */;
|
||||
const quoted = [];
|
||||
const input = _.trimEnd(this.message).replace(/\x08/g, ''); // eslint-disable-line no-control-regex
|
||||
|
||||
// find *last* tearline
|
||||
let tearLinePos = this.getTearLinePosition(input);
|
||||
let tearLinePos = Message.getTearLinePosition(input);
|
||||
tearLinePos = -1 === tearLinePos ? input.length : tearLinePos; // we just want the index or the entire string
|
||||
|
||||
input.slice(0, tearLinePos).split(/\r\n\r\n|\n\n/).forEach(paragraph => {
|
||||
|
@ -910,7 +910,7 @@ module.exports = class Message {
|
|||
|
||||
if(quoted.length > 0) {
|
||||
//
|
||||
// Preserve paragraph seperation.
|
||||
// Preserve paragraph separation.
|
||||
//
|
||||
// FSC-0032 states something about leaving blank lines fully blank
|
||||
// (without a prefix) but it seems nicer (and more consistent with other systems)
|
||||
|
|
|
@ -426,7 +426,8 @@ exports.getModule = class mrcModule extends MenuModule {
|
|||
|
||||
switch (cmd[0]) {
|
||||
case 'pm':
|
||||
this.processOutgoingMessage(cmd[2], cmd[1]);
|
||||
const newmsg = cmd.slice(2).join(' ');
|
||||
this.processOutgoingMessage(newmsg, cmd[1]);
|
||||
break;
|
||||
|
||||
case 'rainbow': {
|
||||
|
|
|
@ -65,7 +65,7 @@ const _ = require('lodash');
|
|||
const SPECIAL_KEY_MAP_DEFAULT = {
|
||||
'line feed' : [ 'return' ],
|
||||
exit : [ 'esc' ],
|
||||
backspace : [ 'backspace' ],
|
||||
backspace : [ 'backspace', 'ctrl + d' ], // https://www.tecmint.com/linux-command-line-bash-shortcut-keys/
|
||||
delete : [ 'delete' ],
|
||||
tab : [ 'tab' ],
|
||||
up : [ 'up arrow' ],
|
||||
|
@ -74,7 +74,7 @@ const SPECIAL_KEY_MAP_DEFAULT = {
|
|||
home : [ 'home' ],
|
||||
left : [ 'left arrow' ],
|
||||
right : [ 'right arrow' ],
|
||||
'delete line' : [ 'ctrl + y' ],
|
||||
'delete line' : [ 'ctrl + y', 'ctrl + u' ], // https://en.wikipedia.org/wiki/Backspace
|
||||
'page up' : [ 'page up' ],
|
||||
'page down' : [ 'page down' ],
|
||||
insert : [ 'insert', 'ctrl + v' ],
|
||||
|
@ -265,11 +265,10 @@ function MultiLineEditTextView(options) {
|
|||
|
||||
this.getRenderText = function(index) {
|
||||
let text = self.getVisibleText(index);
|
||||
const remain = self.dimens.width - text.length;
|
||||
const remain = self.dimens.width - strUtil.renderStringLength(text);
|
||||
|
||||
if(remain > 0) {
|
||||
text += ' '.repeat(remain + 1);
|
||||
// text += new Array(remain + 1).join(' ');
|
||||
text += ' '.repeat(remain);// + 1);
|
||||
}
|
||||
|
||||
return text;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -357,9 +357,11 @@ exports.getModule = class SSHServerModule extends LoginServerModule {
|
|||
// However, as of this writing, NetRunner and SyncTERM both
|
||||
// fail to respond to OpenSSH keep-alive pings (keepalive@openssh.com)
|
||||
//
|
||||
ssh2.Server.KEEPALIVE_INTERVAL = 0;
|
||||
// See also #399
|
||||
//
|
||||
ssh2.Server.KEEPALIVE_CLIENT_INTERVAL = 0;
|
||||
|
||||
this.server = ssh2.Server(serverConf);
|
||||
this.server = new ssh2.Server(serverConf);
|
||||
this.server.on('connection', (conn, info) => {
|
||||
Log.info(info, 'New SSH connection');
|
||||
this.handleNewClient(new SSHClient(conn), conn._sock, ModuleInfo);
|
||||
|
|
|
@ -103,14 +103,16 @@ class TelnetClient {
|
|||
case Options.NEW_ENVIRON :
|
||||
{
|
||||
this._logDebug(
|
||||
{ vars : command.optionData.vars, userVars : command.optionData.userVars },
|
||||
{ vars : command.optionData.vars, uservars : command.optionData.uservars },
|
||||
'New environment received'
|
||||
);
|
||||
|
||||
// get a value from vars with fallback of user vars
|
||||
const getValue = (name) => {
|
||||
return command.optionData.vars.find(nv => nv.name === name) ||
|
||||
command.optionData.userVars.find(nv => nv.name === name);
|
||||
return command.optionData.vars &&
|
||||
(command.optionData.vars.find(nv => nv.name === name) ||
|
||||
command.optionData.uservars.find(nv => nv.name === name)
|
||||
);
|
||||
};
|
||||
|
||||
if ('unknown' === this.term.termType) {
|
||||
|
|
|
@ -171,22 +171,15 @@ exports.getModule = class ShowArtModule extends MenuModule {
|
|||
return callback(err);
|
||||
}
|
||||
const mciData = { menu : artData.mciMap };
|
||||
return callback(null, mciData);
|
||||
if(self.client.term.termHeight > 0 && artData.height > self.client.term.termHeight) {
|
||||
// We must have scrolled, adjust the positioning for pause
|
||||
artData.height = self.client.term.termHeight;
|
||||
}
|
||||
const pausePosition = { row: artData.height + 1, col: 1};
|
||||
return callback(null, mciData, pausePosition);
|
||||
}
|
||||
);
|
||||
},
|
||||
function recordCursorPosition(mciData, callback) {
|
||||
if(!options.pause) {
|
||||
return callback(null, mciData, null); // cursor position not needed
|
||||
}
|
||||
|
||||
self.client.once('cursor position report', pos => {
|
||||
const pausePosition = { row : pos[0], col : 1 };
|
||||
return callback(null, mciData, pausePosition);
|
||||
});
|
||||
|
||||
self.client.term.rawWrite(ANSI.queryPos());
|
||||
},
|
||||
function afterArtDisplayed(mciData, pausePosition, callback) {
|
||||
self.mciReady(mciData, err => {
|
||||
return callback(err, pausePosition);
|
||||
|
|
|
@ -20,6 +20,7 @@ exports.stringFromNullTermBuffer = stringFromNullTermBuffer;
|
|||
exports.stringToNullTermBuffer = stringToNullTermBuffer;
|
||||
exports.renderSubstr = renderSubstr;
|
||||
exports.renderStringLength = renderStringLength;
|
||||
exports.ansiRenderStringLength = ansiRenderStringLength;
|
||||
exports.formatByteSizeAbbr = formatByteSizeAbbr;
|
||||
exports.formatByteSize = formatByteSize;
|
||||
exports.formatCountAbbr = formatCountAbbr;
|
||||
|
@ -297,7 +298,7 @@ function renderStringLength(s) {
|
|||
let len = 0;
|
||||
|
||||
const re = ANSI_OR_PIPE_REGEXP;
|
||||
re.lastIndex = 0; // we recycle the rege; reset
|
||||
re.lastIndex = 0; // we recycle the regex; reset
|
||||
|
||||
//
|
||||
// Loop counting only literal (non-control) sequences
|
||||
|
@ -312,7 +313,41 @@ function renderStringLength(s) {
|
|||
len += s.slice(pos, m.index).length;
|
||||
}
|
||||
|
||||
if('C' === m[3]) { // ESC[<N>C is foward/right
|
||||
if('C' === m[3]) { // ESC[<N>C is forward/right
|
||||
len += parseInt(m[2], 10) || 0;
|
||||
}
|
||||
}
|
||||
} while(0 !== re.lastIndex);
|
||||
|
||||
if(pos < s.length) {
|
||||
len += s.slice(pos).length;
|
||||
}
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
// Like renderStringLength() but ANSI only (no pipe codes accounted for)
|
||||
function ansiRenderStringLength(s) {
|
||||
let m;
|
||||
let pos;
|
||||
let len = 0;
|
||||
|
||||
const re = ANSI.getFullMatchRegExp();
|
||||
|
||||
//
|
||||
// Loop counting only literal (non-control) sequences
|
||||
// paying special attention to ESC[<N>C which means forward <N>
|
||||
//
|
||||
do {
|
||||
pos = re.lastIndex;
|
||||
m = re.exec(s);
|
||||
|
||||
if(m) {
|
||||
if(m.index > pos) {
|
||||
len += s.slice(pos, m.index).length;
|
||||
}
|
||||
|
||||
if('C' === m[3]) { // ESC[<N>C is forward/right
|
||||
len += parseInt(m[2], 10) || 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ exports.nextConf = nextConf;
|
|||
exports.prevArea = prevArea;
|
||||
exports.nextArea = nextArea;
|
||||
exports.sendForgotPasswordEmail = sendForgotPasswordEmail;
|
||||
exports.optimizeDatabases = optimizeDatabases;
|
||||
|
||||
const handleAuthFailures = (callingMenu, err, cb) => {
|
||||
// already logged in with this user?
|
||||
|
@ -205,3 +206,25 @@ function sendForgotPasswordEmail(callingMenu, formData, extraArgs, cb) {
|
|||
return logoff(callingMenu, formData, extraArgs, cb);
|
||||
});
|
||||
}
|
||||
|
||||
function optimizeDatabases(callingMenu, formData, extraArgs, cb) {
|
||||
const dbs = require('./database').dbs;
|
||||
const client = callingMenu.client;
|
||||
|
||||
client.term.write('\r\n\r\n');
|
||||
|
||||
Object.keys(dbs).forEach(dbName => {
|
||||
client.log.info({ dbName }, 'Optimizing database');
|
||||
|
||||
client.term.write(`Optimizing ${dbName}. Please wait...\r\n`);
|
||||
|
||||
// https://www.sqlite.org/pragma.html#pragma_optimize
|
||||
dbs[dbName].run('PRAGMA optimize;', err => {
|
||||
if (err) {
|
||||
client.log.error({ error : err, dbName }, 'Error attempting to optimize database');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return callingMenu.prevMenu(cb);
|
||||
}
|
|
@ -44,49 +44,6 @@ function TextView(options) {
|
|||
this.textMaskChar = options.textMaskChar;
|
||||
}
|
||||
|
||||
/*
|
||||
this.drawText = function(s) {
|
||||
|
||||
//
|
||||
// |<- this.maxLength
|
||||
// ABCDEFGHIJK
|
||||
// |ABCDEFG| ^_ this.text.length
|
||||
// ^-- this.dimens.width
|
||||
//
|
||||
let textToDraw = _.isString(this.textMaskChar) ?
|
||||
new Array(s.length + 1).join(this.textMaskChar) :
|
||||
stylizeString(s, this.hasFocus ? this.focusTextStyle : this.textStyle);
|
||||
|
||||
if(textToDraw.length > this.dimens.width) {
|
||||
if(this.hasFocus) {
|
||||
if(this.horizScroll) {
|
||||
textToDraw = textToDraw.substr(textToDraw.length - this.dimens.width, textToDraw.length);
|
||||
}
|
||||
} else {
|
||||
if(textToDraw.length > this.dimens.width) {
|
||||
if(this.textOverflow &&
|
||||
this.dimens.width > this.textOverflow.length &&
|
||||
textToDraw.length - this.textOverflow.length >= this.textOverflow.length)
|
||||
{
|
||||
textToDraw = textToDraw.substr(0, this.dimens.width - this.textOverflow.length) + this.textOverflow;
|
||||
} else {
|
||||
textToDraw = textToDraw.substr(0, this.dimens.width);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.client.term.write(padStr(
|
||||
textToDraw,
|
||||
this.dimens.width + 1,
|
||||
this.fillChar,
|
||||
this.justify,
|
||||
this.hasFocus ? this.getFocusSGR() : this.getSGR(),
|
||||
this.getStyleSGR(1) || this.getSGR()
|
||||
), false);
|
||||
};
|
||||
*/
|
||||
|
||||
this.drawText = function(s) {
|
||||
|
||||
//
|
||||
|
@ -125,7 +82,7 @@ function TextView(options) {
|
|||
this.client.term.write(
|
||||
padStr(
|
||||
textToDraw,
|
||||
this.dimens.width + 1,
|
||||
this.dimens.width,
|
||||
renderedFillChar, //this.fillChar,
|
||||
this.justify,
|
||||
this.hasFocus ? this.getFocusSGR() : this.getSGR(),
|
||||
|
|
|
@ -495,6 +495,7 @@ function displayPreparedArt(options, artInfo, cb) {
|
|||
sauce : artInfo.sauce,
|
||||
font : options.font,
|
||||
trailingLF : options.trailingLF,
|
||||
startRow : options.startRow,
|
||||
};
|
||||
art.display(options.client, artInfo.data, displayOpts, (err, mciMap, extraInfo) => {
|
||||
return cb(err, { mciMap : mciMap, artInfo : artInfo, extraInfo : extraInfo } );
|
||||
|
@ -551,6 +552,7 @@ function displayThemedPrompt(name, client, options, cb) {
|
|||
|
||||
if(options.clearScreen) {
|
||||
client.term.rawWrite(ansi.resetScreen());
|
||||
options.position = {row: 1, column: 1};
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -560,9 +562,9 @@ function displayThemedPrompt(name, client, options, cb) {
|
|||
//
|
||||
const dispOptions = Object.assign( {}, options, promptConfig.config );
|
||||
// :TODO: We can use term detection to do nifty things like avoid this kind of kludge:
|
||||
if(!options.clearScreen) {
|
||||
dispOptions.font = 'not_really_a_font!'; // kludge :)
|
||||
}
|
||||
// if(!options.clearScreen) {
|
||||
// dispOptions.font = 'not_really_a_font!'; // kludge :)
|
||||
// }
|
||||
|
||||
displayThemedAsset(
|
||||
promptConfig.art,
|
||||
|
@ -583,12 +585,15 @@ function displayThemedPrompt(name, client, options, cb) {
|
|||
return callback(null, promptConfig, artInfo);
|
||||
}
|
||||
|
||||
client.once('cursor position report', pos => {
|
||||
artInfo.startRow = pos[0] - artInfo.height;
|
||||
return callback(null, promptConfig, artInfo);
|
||||
});
|
||||
if(_.isNumber(options?.position?.row)) {
|
||||
artInfo.startRow = options.position.row;
|
||||
if(client.term.termHeight > 0 && artInfo.startRow + artInfo.height > client.term.termHeight) {
|
||||
// in this case, we will have scrolled
|
||||
artInfo.startRow = client.term.termHeight - artInfo.height;
|
||||
}
|
||||
}
|
||||
|
||||
client.term.rawWrite(ansi.queryPos());
|
||||
return callback(null, promptConfig, artInfo);
|
||||
},
|
||||
function createMCIViews(promptConfig, artInfo, callback) {
|
||||
const assocViewController = usingTempViewController ? new ViewController( { client : client } ) : options.viewController;
|
||||
|
@ -614,7 +619,9 @@ function displayThemedPrompt(name, client, options, cb) {
|
|||
});
|
||||
},
|
||||
function clearPauseArt(artInfo, assocViewController, callback) {
|
||||
if(options.clearPrompt) {
|
||||
// Only clear with height if clearPrompt is true and if we were able
|
||||
// to determine the row
|
||||
if(options.clearPrompt && artInfo.startRow) {
|
||||
if(artInfo.startRow && artInfo.height) {
|
||||
client.term.rawWrite(ansi.goto(artInfo.startRow, 1));
|
||||
|
||||
|
|
|
@ -332,6 +332,7 @@ exports.getModule = class UploadModule extends MenuModule {
|
|||
const scanOpts = {
|
||||
areaTag : self.areaInfo.areaTag,
|
||||
storageTag : self.areaInfo.storageTags[0],
|
||||
hashTags : self.areaInfo.hashTags,
|
||||
};
|
||||
|
||||
function handleScanStep(stepInfo, nextScanStep) {
|
||||
|
|
18
core/view.js
18
core/view.js
|
@ -17,7 +17,7 @@ exports.View = View;
|
|||
const VIEW_SPECIAL_KEY_MAP_DEFAULT = {
|
||||
accept : [ 'return' ],
|
||||
exit : [ 'esc' ],
|
||||
backspace : [ 'backspace', 'del' ],
|
||||
backspace : [ 'backspace', 'del', 'ctrl + d'], // https://www.tecmint.com/linux-command-line-bash-shortcut-keys/
|
||||
del : [ 'del' ],
|
||||
next : [ 'tab' ],
|
||||
up : [ 'up arrow' ],
|
||||
|
@ -154,7 +154,7 @@ View.prototype.setHeight = function(height) {
|
|||
|
||||
View.prototype.setWidth = function(width) {
|
||||
width = parseInt(width) || 1;
|
||||
width = Math.min(width, this.client.term.termWidth);
|
||||
width = Math.min(width, this.client.term.termWidth - this.position.col);
|
||||
|
||||
this.dimens.width = width;
|
||||
};
|
||||
|
@ -186,7 +186,7 @@ View.prototype.setPropertyValue = function(propName, value) {
|
|||
|
||||
case 'height' : this.setHeight(value); break;
|
||||
case 'width' : this.setWidth(value); break;
|
||||
case 'focus' : this.setFocus(value); break;
|
||||
case 'focus' : this.setFocusProperty(value); break;
|
||||
|
||||
case 'text' :
|
||||
if('setText' in this) {
|
||||
|
@ -252,10 +252,16 @@ View.prototype.redraw = function() {
|
|||
this.client.term.write(ansi.goto(this.position.row, this.position.col));
|
||||
};
|
||||
|
||||
View.prototype.setFocus = function(focused) {
|
||||
enigAssert(this.acceptsFocus, 'View does not accept focus');
|
||||
|
||||
View.prototype.setFocusProperty = function(focused) {
|
||||
// Either this should accept focus, or the focus should be false
|
||||
enigAssert(this.acceptsFocus || !focused, 'View does not accept focus');
|
||||
this.hasFocus = focused;
|
||||
};
|
||||
|
||||
View.prototype.setFocus = function(focused) {
|
||||
// Call separate method to differentiate between a value set as a
|
||||
// property vs focus programmatically called.
|
||||
this.setFocusProperty(focused);
|
||||
this.restoreCursor();
|
||||
};
|
||||
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
const renderStringLength = require('./string_util.js').renderStringLength;
|
||||
const {
|
||||
ansiRenderStringLength,
|
||||
} = require('./string_util');
|
||||
|
||||
// deps
|
||||
const assert = require('assert');
|
||||
|
@ -28,7 +30,7 @@ function wordWrapText(text, options) {
|
|||
|
||||
//const REGEXP_GOBBLE = new RegExp(`.{0,${options.width}}`, 'g');
|
||||
//
|
||||
// For a given word, match 0->options.width chars -- alwasy include a full trailing ESC
|
||||
// For a given word, match 0->options.width chars -- always include a full trailing ESC
|
||||
// sequence if present!
|
||||
//
|
||||
// :TODO: Need to create ansi.getMatchRegex or something - this is used all over
|
||||
|
@ -49,7 +51,7 @@ function wordWrapText(text, options) {
|
|||
|
||||
function appendWord() {
|
||||
word.match(REGEXP_GOBBLE).forEach( w => {
|
||||
renderLen = renderStringLength(w);
|
||||
renderLen = ansiRenderStringLength(w);
|
||||
|
||||
if(result.renderLen[i] + renderLen > options.width) {
|
||||
if(0 === i) {
|
||||
|
@ -70,7 +72,7 @@ function wordWrapText(text, options) {
|
|||
//
|
||||
// * Sublime Text 3 for example considers spaces after a word
|
||||
// part of said word. For example, "word " would be wraped
|
||||
// in it's entirity.
|
||||
// in it's entirety.
|
||||
//
|
||||
// * Tabs in Sublime Text 3 are also treated as a word, so, e.g.
|
||||
// "\t" may resolve to " " and must fit within the space.
|
||||
|
|
|
@ -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"]
|
|
@ -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
|
Binary file not shown.
15
docs/Gemfile
15
docs/Gemfile
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
105
docs/_config.yml
105
docs/_config.yml
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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.
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
|
@ -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.
|
||||
|
|
@ -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" ]
|
||||
}
|
||||
}
|
||||
```
|
|
@ -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 .
|
||||
```
|
|
@ -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:
|
||||
|
|
@ -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
Loading…
Reference in New Issue