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,
|
"comma-dangle": 0,
|
||||||
"no-trailing-spaces" :"warn"
|
"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.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
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
|
## Support
|
||||||
* See [Discussions](https://github.com/NuSkooler/enigma-bbs/discussions) and [the issue tracker](https://github.com/NuSkooler/enigma-bbs/issues)
|
* 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)
|
* **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
|
* 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
|
* Email: bryan -at- l33t.codes
|
||||||
* [Facebook ENiGMA½ group](https://www.facebook.com/groups/enigmabbs/)
|
* [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**)
|
* [fORCE9](http://bbs.force9.org/): (**telnet://bbs.force9.org**)
|
||||||
* [Undercurrents](https://undercurrents.io): (**ssh://undercurrents.io**)
|
* [Undercurrents](https://undercurrents.io): (**ssh://undercurrents.io**)
|
||||||
* [PlaneT Afr0](https://planetafr0.org/): (**ssh://planetafr0.org:8889**)
|
* [PlaneT Afr0](https://planetafr0.org/): (**ssh://planetafr0.org:8889**)
|
||||||
* [Goblin Studio](https://goblin.strangled.net): (**ssh://goblin.strangled.net:8889**)
|
|
||||||
|
|
||||||
## Special Thanks
|
## 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.
|
* [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
|
* [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!)
|
* [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)!
|
* [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!
|
* [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)?
|
* [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)!
|
* 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!
|
...and so many others! This project would be nothing without the BBS and artscene communities!
|
||||||
|
|
||||||
## License
|
## License
|
||||||
Released under the [BSD 2-clause](https://opensource.org/licenses/BSD-2-Clause) 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.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
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
|
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).
|
[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
|
# 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.
|
* 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:
|
* **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
|
# Whats New
|
||||||
This document attempts to track **major** changes and additions in ENiGMA½. For details, see GitHub.
|
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
|
## 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).
|
* 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).
|
* 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`.
|
* 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.
|
* 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
|
* `./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`).
|
* 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.
|
* 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
|
## 0.0.11-beta
|
||||||
* Upgraded from `alpha` to `beta` -- The software is far along and mature enough at this point!
|
* 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
|
// The 'msg_list' module looks for this entry by default
|
||||||
messageAreaViewPost: {
|
messageAreaViewPost: {
|
||||||
|
config: {
|
||||||
|
quoteStyleLevel1: [
|
||||||
|
"|00|11",
|
||||||
|
"|00|08",
|
||||||
|
"|00|03",
|
||||||
|
]
|
||||||
|
tearLineStyle: [
|
||||||
|
"|00|08",
|
||||||
|
"|00|02",
|
||||||
|
]
|
||||||
|
originStyle: [
|
||||||
|
"|00|08",
|
||||||
|
"|00|06",
|
||||||
|
"|00|03",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
0: {
|
0: {
|
||||||
mci: {
|
mci: {
|
||||||
TL1: {
|
TL1: {
|
||||||
|
|
|
@ -11,12 +11,14 @@ const {
|
||||||
trackDoorRunBegin,
|
trackDoorRunBegin,
|
||||||
trackDoorRunEnd
|
trackDoorRunEnd
|
||||||
} = require('./door_util.js');
|
} = require('./door_util.js');
|
||||||
|
const Log = require('./logger').log;
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const async = require('async');
|
const async = require('async');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const paths = require('path');
|
const paths = require('path');
|
||||||
|
const fs = require('graceful-fs');
|
||||||
|
|
||||||
const activeDoorNodeInstances = {};
|
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, ... }
|
// :TODO: MenuModule.validateConfig(cb) -- validate config section gracefully instead of asserts! -- { key : type, key2 : type2, ... }
|
||||||
// .. and/or EnigAssert
|
// .. and/or EnigAssert
|
||||||
assert(_.isString(this.config.name, 'Config \'name\' is required'));
|
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'));
|
assert(_.isString(this.config.cmd, 'Config \'cmd\' is required'));
|
||||||
|
|
||||||
this.config.nodeMax = this.config.nodeMax || 0;
|
this.config.nodeMax = this.config.nodeMax || 0;
|
||||||
this.config.args = this.config.args || [];
|
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() {
|
incrementActiveDoorNodeInstances() {
|
||||||
if(activeDoorNodeInstances[this.config.name]) {
|
if(activeDoorNodeInstances[this.config.name]) {
|
||||||
activeDoorNodeInstances[this.config.name] += 1;
|
activeDoorNodeInstances[this.config.name] += 1;
|
||||||
|
@ -141,11 +135,15 @@ exports.getModule = class AbracadabraModule extends MenuModule {
|
||||||
return self.doorInstance.prepare(self.config.io || 'stdio', callback);
|
return self.doorInstance.prepare(self.config.io || 'stdio', callback);
|
||||||
},
|
},
|
||||||
function generateDropfile(callback) {
|
function generateDropfile(callback) {
|
||||||
const dropFileOpts = {
|
if (!self.config.dropFileType || self.config.dropFileType.toLowerCase() === 'none') {
|
||||||
fileType : self.config.dropFileType,
|
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);
|
return self.dropFile.createFile(callback);
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -170,17 +168,30 @@ exports.getModule = class AbracadabraModule extends MenuModule {
|
||||||
args : this.config.args,
|
args : this.config.args,
|
||||||
io : this.config.io || 'stdio',
|
io : this.config.io || 'stdio',
|
||||||
encoding : this.config.encoding || 'cp437',
|
encoding : this.config.encoding || 'cp437',
|
||||||
dropFile : this.dropFile.fileName,
|
|
||||||
dropFilePath : this.dropFile.fullPath,
|
|
||||||
node : this.client.node,
|
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);
|
const doorTracking = trackDoorRunBegin(this.client, this.config.name);
|
||||||
|
|
||||||
this.doorInstance.run(exeInfo, () => {
|
this.doorInstance.run(exeInfo, () => {
|
||||||
trackDoorRunEnd(doorTracking);
|
trackDoorRunEnd(doorTracking);
|
||||||
this.decrementActiveDoorNodeInstances();
|
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 -
|
// client may have disconnected while process was active -
|
||||||
// we're done here if so.
|
// we're done here if so.
|
||||||
if(!this.client.term.output) {
|
if(!this.client.term.output) {
|
||||||
|
|
|
@ -21,8 +21,6 @@ function ANSIEscapeParser(options) {
|
||||||
events.EventEmitter.call(this);
|
events.EventEmitter.call(this);
|
||||||
|
|
||||||
this.column = 1;
|
this.column = 1;
|
||||||
this.row = 1;
|
|
||||||
this.scrollBack = 0;
|
|
||||||
this.graphicRendition = {};
|
this.graphicRendition = {};
|
||||||
|
|
||||||
this.parseState = {
|
this.parseState = {
|
||||||
|
@ -36,11 +34,15 @@ function ANSIEscapeParser(options) {
|
||||||
trailingLF : 'default', // default|omit|no|yes, ...
|
trailingLF : 'default', // default|omit|no|yes, ...
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
this.mciReplaceChar = miscUtil.valueWithDefault(options.mciReplaceChar, '');
|
this.mciReplaceChar = miscUtil.valueWithDefault(options.mciReplaceChar, '');
|
||||||
this.termHeight = miscUtil.valueWithDefault(options.termHeight, 25);
|
this.termHeight = miscUtil.valueWithDefault(options.termHeight, 25);
|
||||||
this.termWidth = miscUtil.valueWithDefault(options.termWidth, 80);
|
this.termWidth = miscUtil.valueWithDefault(options.termWidth, 80);
|
||||||
this.trailingLF = miscUtil.valueWithDefault(options.trailingLF, 'default');
|
this.trailingLF = miscUtil.valueWithDefault(options.trailingLF, 'default');
|
||||||
|
|
||||||
|
|
||||||
|
this.row = Math.min(options?.startRow ?? 1, this.termHeight);
|
||||||
|
|
||||||
self.moveCursor = function(cols, rows) {
|
self.moveCursor = function(cols, rows) {
|
||||||
self.column += cols;
|
self.column += cols;
|
||||||
self.row += rows;
|
self.row += rows;
|
||||||
|
@ -69,14 +71,11 @@ function ANSIEscapeParser(options) {
|
||||||
};
|
};
|
||||||
|
|
||||||
self.clearScreen = function() {
|
self.clearScreen = function() {
|
||||||
// :TODO: should be doing something with row/column?
|
self.column = 1;
|
||||||
|
self.row = 1;
|
||||||
self.emit('clear screen');
|
self.emit('clear screen');
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
|
||||||
self.rowUpdated = function() {
|
|
||||||
self.emit('row update', self.row + self.scrollBack);
|
|
||||||
};*/
|
|
||||||
|
|
||||||
self.positionUpdated = function() {
|
self.positionUpdated = function() {
|
||||||
self.emit('position update', self.row, self.column);
|
self.emit('position update', self.row, self.column);
|
||||||
|
@ -190,6 +189,7 @@ function ANSIEscapeParser(options) {
|
||||||
|
|
||||||
|
|
||||||
self.emit('mci', {
|
self.emit('mci', {
|
||||||
|
position : [self.row, self.column],
|
||||||
mci : mciCode,
|
mci : mciCode,
|
||||||
id : id ? parseInt(id, 10) : null,
|
id : id ? parseInt(id, 10) : null,
|
||||||
args : args,
|
args : args,
|
||||||
|
@ -215,6 +215,9 @@ function ANSIEscapeParser(options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.reset = function(input) {
|
self.reset = function(input) {
|
||||||
|
self.column = 1;
|
||||||
|
self.row = Math.min(options?.startRow ?? 1, self.termHeight);
|
||||||
|
|
||||||
self.parseState = {
|
self.parseState = {
|
||||||
// ignore anything past EOF marker, if any
|
// ignore anything past EOF marker, if any
|
||||||
buffer : input.split(String.fromCharCode(0x1a), 1)[0],
|
buffer : input.split(String.fromCharCode(0x1a), 1)[0],
|
||||||
|
|
|
@ -309,7 +309,8 @@ const FONT_ALIAS_TO_SYNCTERM_MAP = {
|
||||||
|
|
||||||
'mo_soul' : 'mo_soul',
|
'mo_soul' : 'mo_soul',
|
||||||
'mosoul' : 'mo_soul',
|
'mosoul' : 'mo_soul',
|
||||||
'mO\'sOul' : 'mo_soul',
|
'mo\'soul' : 'mo_soul',
|
||||||
|
'amiga_mosoul' : 'mo_soul',
|
||||||
|
|
||||||
'amiga_microknight' : 'microknight',
|
'amiga_microknight' : 'microknight',
|
||||||
'amiga_microknight+' : 'microknight_plus',
|
'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,
|
termHeight : client.term.termHeight,
|
||||||
termWidth : client.term.termWidth,
|
termWidth : client.term.termWidth,
|
||||||
trailingLF : options.trailingLF,
|
trailingLF : options.trailingLF,
|
||||||
|
startRow : options.startRow,
|
||||||
});
|
});
|
||||||
|
|
||||||
let parseComplete = false;
|
let parseComplete = false;
|
||||||
let cprListener;
|
|
||||||
let mciMap;
|
let mciMap;
|
||||||
const mciCprQueue = [];
|
const mciCprQueue = [];
|
||||||
let artHash;
|
let artHash;
|
||||||
let mciMapFromCache;
|
let mciMapFromCache;
|
||||||
|
|
||||||
function completed() {
|
function completed() {
|
||||||
if(cprListener) {
|
|
||||||
client.removeListener('cursor position report', cprListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!options.disableMciCache && !mciMapFromCache) {
|
if(!options.disableMciCache && !mciMapFromCache) {
|
||||||
// cache our MCI findings...
|
// cache our MCI findings...
|
||||||
|
@ -314,18 +311,6 @@ function display(client, art, options, cb) {
|
||||||
// no cached MCI info
|
// no cached MCI info
|
||||||
mciMap = {};
|
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;
|
let generatedId = 100;
|
||||||
|
|
||||||
ansiParser.on('mci', mciInfo => {
|
ansiParser.on('mci', mciInfo => {
|
||||||
|
@ -339,6 +324,7 @@ function display(client, art, options, cb) {
|
||||||
mapEntry.focusArgs = mciInfo.args;
|
mapEntry.focusArgs = mciInfo.args;
|
||||||
} else {
|
} else {
|
||||||
mciMap[mapKey] = {
|
mciMap[mapKey] = {
|
||||||
|
position : mciInfo.position,
|
||||||
args : mciInfo.args,
|
args : mciInfo.args,
|
||||||
SGR : mciInfo.SGR,
|
SGR : mciInfo.SGR,
|
||||||
code : mciInfo.mci,
|
code : mciInfo.mci,
|
||||||
|
@ -348,9 +334,6 @@ function display(client, art, options, cb) {
|
||||||
if(!mciInfo.id) {
|
if(!mciInfo.id) {
|
||||||
++generatedId;
|
++generatedId;
|
||||||
}
|
}
|
||||||
|
|
||||||
mciCprQueue.push(mapKey);
|
|
||||||
client.term.rawWrite(ansi.queryPos());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -21,7 +21,7 @@ function ButtonView(options) {
|
||||||
util.inherits(ButtonView, TextView);
|
util.inherits(ButtonView, TextView);
|
||||||
|
|
||||||
ButtonView.prototype.onKeyPress = function(ch, key) {
|
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.submitData = 'accept';
|
||||||
this.emit('action', 'accept');
|
this.emit('action', 'accept');
|
||||||
delete this.submitData;
|
delete this.submitData;
|
||||||
|
@ -29,16 +29,6 @@ ButtonView.prototype.onKeyPress = function(ch, key) {
|
||||||
ButtonView.super_.prototype.onKeyPress.call(this, 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() {
|
ButtonView.prototype.getData = function() {
|
||||||
return this.submitData || null;
|
return this.submitData || null;
|
||||||
|
|
|
@ -4,11 +4,12 @@
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
var Log = require('./logger.js').log;
|
var Log = require('./logger.js').log;
|
||||||
var renegadeToAnsi = require('./color_codes.js').renegadeToAnsi;
|
var renegadeToAnsi = require('./color_codes.js').renegadeToAnsi;
|
||||||
|
const Config = require('./config.js').get;
|
||||||
var iconv = require('iconv-lite');
|
var iconv = require('iconv-lite');
|
||||||
var assert = require('assert');
|
var assert = require('assert');
|
||||||
var _ = require('lodash');
|
var _ = require('lodash');
|
||||||
|
|
||||||
|
|
||||||
exports.ClientTerminal = ClientTerminal;
|
exports.ClientTerminal = ClientTerminal;
|
||||||
|
|
||||||
function ClientTerminal(output) {
|
function ClientTerminal(output) {
|
||||||
|
@ -115,7 +116,8 @@ ClientTerminal.prototype.isNixTerm = function() {
|
||||||
return true;
|
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() {
|
ClientTerminal.prototype.isANSI = function() {
|
||||||
|
@ -153,7 +155,8 @@ ClientTerminal.prototype.isANSI = function() {
|
||||||
// linux:
|
// linux:
|
||||||
// * JuiceSSH (note: TERM=linux also)
|
// * 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)
|
// :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
|
// 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.
|
// MCI codes.
|
||||||
//
|
//
|
||||||
// Supported control code formats:
|
// 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
|
// * 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
|
// * WildCat! : @##@ the same as PCBoard without the X prefix, but with a @ suffix
|
||||||
// * WWIV : ^#
|
// * WWIV : ^#
|
||||||
// * CNET Y-Style : 0x19## where ## is a specific set of codes -- this is the older format
|
// * CNET Control-Y: AKA Y-Style -- 0x19## where ## is a specific set of codes (older format)
|
||||||
// * CNET Q-style : 0x11##} where ## is a specific set of codes -- this is the newer format
|
// * CNET Control-Q: AKA Q-style -- 0x11##} where ## is a specific set of codes (newer format)
|
||||||
//
|
//
|
||||||
// TODO: Add Synchronet and Celerity format support
|
// TODO: Add Synchronet and Celerity format support
|
||||||
//
|
//
|
||||||
// Resources:
|
// Resources:
|
||||||
// * http://wiki.synchro.net/custom:colors
|
// * 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) {
|
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 m;
|
||||||
let result = '';
|
let result = '';
|
||||||
|
|
|
@ -17,6 +17,24 @@ module.exports = () => {
|
||||||
achievementFile : 'achievements.hjson',
|
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 : {
|
users : {
|
||||||
usernameMin : 2,
|
usernameMin : 2,
|
||||||
usernameMax : 16, // Note that FidoNet wants 36 max
|
usernameMax : 16, // Note that FidoNet wants 36 max
|
||||||
|
@ -166,10 +184,11 @@ module.exports = () => {
|
||||||
'ecdh-sha2-nistp256',
|
'ecdh-sha2-nistp256',
|
||||||
'ecdh-sha2-nistp384',
|
'ecdh-sha2-nistp384',
|
||||||
'ecdh-sha2-nistp521',
|
'ecdh-sha2-nistp521',
|
||||||
'diffie-hellman-group-exchange-sha256',
|
|
||||||
'diffie-hellman-group14-sha1',
|
'diffie-hellman-group14-sha1',
|
||||||
'diffie-hellman-group-exchange-sha1',
|
|
||||||
'diffie-hellman-group1-sha1',
|
'diffie-hellman-group1-sha1',
|
||||||
|
// Group exchange not currnetly supported
|
||||||
|
// 'diffie-hellman-group-exchange-sha256',
|
||||||
|
// 'diffie-hellman-group-exchange-sha1',
|
||||||
],
|
],
|
||||||
cipher : [
|
cipher : [
|
||||||
'aes128-ctr',
|
'aes128-ctr',
|
||||||
|
@ -492,7 +511,7 @@ module.exports = () => {
|
||||||
},
|
},
|
||||||
decompress : {
|
decompress : {
|
||||||
cmd : '7za',
|
cmd : '7za',
|
||||||
args : [ 'e', '-o{extractPath}', '{archivePath}' ] // :TODO: should be 'x'?
|
args : [ 'e', '-y', '-o{extractPath}', '{archivePath}' ] // :TODO: should be 'x'?
|
||||||
},
|
},
|
||||||
list : {
|
list : {
|
||||||
cmd : '7za',
|
cmd : '7za',
|
||||||
|
@ -501,7 +520,7 @@ module.exports = () => {
|
||||||
},
|
},
|
||||||
extract : {
|
extract : {
|
||||||
cmd : '7za',
|
cmd : '7za',
|
||||||
args : [ 'e', '-o{extractPath}', '{archivePath}', '{fileList}' ],
|
args : [ 'e', '-y', '-o{extractPath}', '{archivePath}', '{fileList}' ],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
// ENiGMA½
|
// ENiGMA½
|
||||||
const ansi = require('./ansi_term.js');
|
const ansi = require('./ansi_term.js');
|
||||||
const Events = require('./events.js');
|
const Events = require('./events.js');
|
||||||
|
const Config = require('./config.js').get;
|
||||||
const { Errors } = require('./enig_error.js');
|
const { Errors } = require('./enig_error.js');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
|
@ -18,6 +19,13 @@ function ansiDiscoverHomePosition(client, cb) {
|
||||||
// think of home as 0,0. If this is the case, we need to offset
|
// think of home as 0,0. If this is the case, we need to offset
|
||||||
// our positioning to accommodate for such.
|
// 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) => {
|
const done = (err) => {
|
||||||
client.removeListener('cursor position report', cprListener);
|
client.removeListener('cursor position report', cprListener);
|
||||||
clearTimeout(giveUpTimer);
|
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
|
// We currently only do this if the term hasn't already been ID'd as a
|
||||||
// "*nix" terminal -- that is, xterm, etc.
|
// "*nix" terminal -- that is, xterm, etc.
|
||||||
//
|
// Also skip this check if checkUtf8Encoding is disabled in the config
|
||||||
if(!client.term.isNixTerm()) {
|
|
||||||
|
if(!client.term.isNixTerm() || !Config().term.checkUtf8Encoding) {
|
||||||
return cb(null);
|
return cb(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,6 +128,8 @@ function ansiAttemptDetectUTF8(client, cb) {
|
||||||
return giveUp();
|
return giveUp();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
client.once('cursor position report', cprListener);
|
client.once('cursor position report', cprListener);
|
||||||
client.term.rawWrite(ansi.goHome() + ansi.queryPos());
|
client.term.rawWrite(ansi.goHome() + ansi.queryPos());
|
||||||
}
|
}
|
||||||
|
@ -199,7 +210,7 @@ function displayBanner(term) {
|
||||||
// note: intentional formatting:
|
// note: intentional formatting:
|
||||||
term.pipeWrite(`
|
term.pipeWrite(`
|
||||||
|06Connected to |02EN|10i|02GMA|10½ |06BBS version |12|VN
|
|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/
|
|06Updates & source |14- |12https://github.com/NuSkooler/enigma-bbs/
|
||||||
|00`
|
|00`
|
||||||
);
|
);
|
||||||
|
@ -216,7 +227,6 @@ function connectEntry(client, nextMenu) {
|
||||||
},
|
},
|
||||||
function discoverHomePosition(callback) {
|
function discoverHomePosition(callback) {
|
||||||
ansiDiscoverHomePosition(client, () => {
|
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
|
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 miscUtil = require('./misc_util.js');
|
||||||
const strUtil = require('./string_util.js');
|
const strUtil = require('./string_util.js');
|
||||||
|
|
||||||
|
const VIEW_SPECIAL_KEY_MAP_DEFAULT = require('./view').VIEW_SPECIAL_KEY_MAP_DEFAULT;
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
exports.EditTextView = EditTextView;
|
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) {
|
function EditTextView(options) {
|
||||||
options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true);
|
options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true);
|
||||||
options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true);
|
options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true);
|
||||||
options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block');
|
options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block');
|
||||||
options.resizable = false;
|
options.resizable = false;
|
||||||
|
|
||||||
|
if(!_.isObject(options.specialKeyMap)) {
|
||||||
|
options.specialKeyMap = EDIT_TEXT_VIEW_KEY_MAP;
|
||||||
|
}
|
||||||
|
|
||||||
TextView.call(this, options);
|
TextView.call(this, options);
|
||||||
|
|
||||||
this.initDefaultWidth();
|
this.initDefaultWidth();
|
||||||
|
|
||||||
this.cursorPos = { row : 0, col : 0 };
|
this.cursorPos = { row : 0, col : 0 };
|
||||||
|
|
||||||
this.clientBackspace = function() {
|
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();
|
const fillCharSGR = this.getStyleSGR(1) || this.getSGR();
|
||||||
this.client.term.write(`\b${fillCharSGR}${this.fillChar}\b${this.getFocusSGR()}`);
|
this.client.term.write(`\b${fillCharSGR}${this.fillChar}\b${this.getFocusSGR()}`);
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
require('util').inherits(EditTextView, TextView);
|
require('util').inherits(EditTextView, TextView);
|
||||||
|
@ -35,19 +53,16 @@ EditTextView.prototype.onKeyPress = function(ch, key) {
|
||||||
if(key) {
|
if(key) {
|
||||||
if(this.isKeyMapped('backspace', key.name)) {
|
if(this.isKeyMapped('backspace', key.name)) {
|
||||||
if(this.text.length > 0) {
|
if(this.text.length > 0) {
|
||||||
this.text = this.text.substr(0, this.text.length - 1);
|
|
||||||
|
|
||||||
if(this.text.length >= this.dimens.width) {
|
|
||||||
this.redraw();
|
|
||||||
} else {
|
|
||||||
this.cursorPos.col -= 1;
|
|
||||||
if(this.cursorPos.col >= 0) {
|
|
||||||
this.clientBackspace();
|
this.clientBackspace();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return EditTextView.super_.prototype.onKeyPress.call(this, ch, key);
|
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)) {
|
} else if(this.isKeyMapped('clearLine', key.name)) {
|
||||||
this.text = '';
|
this.text = '';
|
||||||
this.cursorPos.col = 0;
|
this.cursorPos.col = 0;
|
||||||
|
|
|
@ -45,7 +45,7 @@ exports.getFileAreasByTagWildcardRule = getFileAreasByTagWildcardRule;
|
||||||
exports.getFileEntryPath = getFileEntryPath;
|
exports.getFileEntryPath = getFileEntryPath;
|
||||||
exports.changeFileAreaWithOptions = changeFileAreaWithOptions;
|
exports.changeFileAreaWithOptions = changeFileAreaWithOptions;
|
||||||
exports.scanFile = scanFile;
|
exports.scanFile = scanFile;
|
||||||
exports.scanFileAreaForChanges = scanFileAreaForChanges;
|
//exports.scanFileAreaForChanges = scanFileAreaForChanges;
|
||||||
exports.getDescFromFileName = getDescFromFileName;
|
exports.getDescFromFileName = getDescFromFileName;
|
||||||
exports.getAreaStats = getAreaStats;
|
exports.getAreaStats = getAreaStats;
|
||||||
exports.cleanUpTempSessionItems = cleanUpTempSessionItems;
|
exports.cleanUpTempSessionItems = cleanUpTempSessionItems;
|
||||||
|
@ -139,7 +139,14 @@ function getDefaultFileAreaTag(client, disableAcsCheck) {
|
||||||
function getFileAreaByTag(areaTag) {
|
function getFileAreaByTag(areaTag) {
|
||||||
const areaInfo = Config().fileBase.areas[areaTag];
|
const areaInfo = Config().fileBase.areas[areaTag];
|
||||||
if(areaInfo) {
|
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);
|
areaInfo.storage = getAreaStorageLocations(areaInfo);
|
||||||
return areaInfo;
|
return areaInfo;
|
||||||
}
|
}
|
||||||
|
@ -794,7 +801,7 @@ function scanFile(filePath, options, iterator, cb) {
|
||||||
stepInfo.calcHashPercent = Math.round(((stepInfo.bytesProcessed / stepInfo.byteSize) * 100));
|
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;
|
const data = bytesRead < chunkSize ? buffer.slice(0, bytesRead) : buffer;
|
||||||
if(!iterator || stepInfo.calcHashPercent === lastCalcHashPercent) {
|
if(!iterator || stepInfo.calcHashPercent === lastCalcHashPercent) {
|
||||||
|
@ -871,90 +878,91 @@ function scanFile(filePath, options, iterator, cb) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function scanFileAreaForChanges(areaInfo, options, iterator, cb) {
|
// :TODO: this stuff needs cleaned up
|
||||||
if(3 === arguments.length && _.isFunction(iterator)) {
|
// function scanFileAreaForChanges(areaInfo, options, iterator, cb) {
|
||||||
cb = iterator;
|
// if(3 === arguments.length && _.isFunction(iterator)) {
|
||||||
iterator = null;
|
// cb = iterator;
|
||||||
} else if(2 === arguments.length && _.isFunction(options)) {
|
// iterator = null;
|
||||||
cb = options;
|
// } else if(2 === arguments.length && _.isFunction(options)) {
|
||||||
iterator = null;
|
// cb = options;
|
||||||
options = {};
|
// iterator = null;
|
||||||
}
|
// options = {};
|
||||||
|
// }
|
||||||
|
|
||||||
const storageLocations = getAreaStorageLocations(areaInfo);
|
// const storageLocations = getAreaStorageLocations(areaInfo);
|
||||||
|
|
||||||
async.eachSeries(storageLocations, (storageLoc, nextLocation) => {
|
// async.eachSeries(storageLocations, (storageLoc, nextLocation) => {
|
||||||
async.series(
|
// async.series(
|
||||||
[
|
// [
|
||||||
function scanPhysFiles(callback) {
|
// function scanPhysFiles(callback) {
|
||||||
const physDir = storageLoc.dir;
|
// const physDir = storageLoc.dir;
|
||||||
|
|
||||||
fs.readdir(physDir, (err, files) => {
|
// fs.readdir(physDir, (err, files) => {
|
||||||
if(err) {
|
// if(err) {
|
||||||
return callback(err);
|
// return callback(err);
|
||||||
}
|
// }
|
||||||
|
|
||||||
async.eachSeries(files, (fileName, nextFile) => {
|
// async.eachSeries(files, (fileName, nextFile) => {
|
||||||
const fullPath = paths.join(physDir, fileName);
|
// const fullPath = paths.join(physDir, fileName);
|
||||||
|
|
||||||
fs.stat(fullPath, (err, stats) => {
|
// fs.stat(fullPath, (err, stats) => {
|
||||||
if(err) {
|
// if(err) {
|
||||||
// :TODO: Log me!
|
// // :TODO: Log me!
|
||||||
return nextFile(null); // always try next file
|
// return nextFile(null); // always try next file
|
||||||
}
|
// }
|
||||||
|
|
||||||
if(!stats.isFile()) {
|
// if(!stats.isFile()) {
|
||||||
return nextFile(null);
|
// return nextFile(null);
|
||||||
}
|
// }
|
||||||
|
|
||||||
scanFile(
|
// scanFile(
|
||||||
fullPath,
|
// fullPath,
|
||||||
{
|
// {
|
||||||
areaTag : areaInfo.areaTag,
|
// areaTag : areaInfo.areaTag,
|
||||||
storageTag : storageLoc.storageTag
|
// storageTag : storageLoc.storageTag
|
||||||
},
|
// },
|
||||||
iterator,
|
// iterator,
|
||||||
(err, fileEntry, dupeEntries) => {
|
// (err, fileEntry, dupeEntries) => {
|
||||||
if(err) {
|
// if(err) {
|
||||||
// :TODO: Log me!!!
|
// // :TODO: Log me!!!
|
||||||
return nextFile(null); // try next anyway
|
// return nextFile(null); // try next anyway
|
||||||
}
|
// }
|
||||||
|
|
||||||
if(dupeEntries.length > 0) {
|
// if(dupeEntries.length > 0) {
|
||||||
// :TODO: Handle duplidates -- what to do here???
|
// // :TODO: Handle duplicates -- what to do here???
|
||||||
} else {
|
// } else {
|
||||||
if(Array.isArray(options.tags)) {
|
// if(Array.isArray(options.tags)) {
|
||||||
options.tags.forEach(tag => {
|
// options.tags.forEach(tag => {
|
||||||
fileEntry.hashTags.add(tag);
|
// fileEntry.hashTags.add(tag);
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
addNewFileEntry(fileEntry, fullPath, err => {
|
// addNewFileEntry(fileEntry, fullPath, err => {
|
||||||
// pass along error; we failed to insert a record in our DB or something else bad
|
// // pass along error; we failed to insert a record in our DB or something else bad
|
||||||
return nextFile(err);
|
// return nextFile(err);
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
);
|
// );
|
||||||
});
|
// });
|
||||||
}, err => {
|
// }, err => {
|
||||||
return callback(err);
|
// return callback(err);
|
||||||
});
|
// });
|
||||||
});
|
// });
|
||||||
},
|
// },
|
||||||
function scanDbEntries(callback) {
|
// function scanDbEntries(callback) {
|
||||||
// :TODO: Look @ db entries for area that were *not* processed above
|
// // :TODO: Look @ db entries for area that were *not* processed above
|
||||||
return callback(null);
|
// return callback(null);
|
||||||
}
|
// }
|
||||||
],
|
// ],
|
||||||
err => {
|
// err => {
|
||||||
return nextLocation(err);
|
// return nextLocation(err);
|
||||||
}
|
// }
|
||||||
);
|
// );
|
||||||
},
|
// },
|
||||||
err => {
|
// err => {
|
||||||
return cb(err);
|
// return cb(err);
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
function getDescFromFileName(fileName) {
|
function getDescFromFileName(fileName) {
|
||||||
//
|
//
|
||||||
|
|
131
core/fse.js
131
core/fse.js
|
@ -21,7 +21,10 @@ const {
|
||||||
isAnsi, stripAnsiControlCodes,
|
isAnsi, stripAnsiControlCodes,
|
||||||
insert
|
insert
|
||||||
} = require('./string_util.js');
|
} = require('./string_util.js');
|
||||||
const { stripMciColorCodes } = require('./color_codes.js');
|
const {
|
||||||
|
stripMciColorCodes,
|
||||||
|
controlCodesToAnsi,
|
||||||
|
} = require('./color_codes.js');
|
||||||
const Config = require('./config.js').get;
|
const Config = require('./config.js').get;
|
||||||
const { getAddressedToInfo } = require('./mail_util.js');
|
const { getAddressedToInfo } = require('./mail_util.js');
|
||||||
const Events = require('./events.js');
|
const Events = require('./events.js');
|
||||||
|
@ -418,7 +421,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
|
||||||
//
|
//
|
||||||
// Find tearline - we want to color it differently.
|
// Find tearline - we want to color it differently.
|
||||||
//
|
//
|
||||||
const tearLinePos = this.message.getTearLinePosition(msg);
|
const tearLinePos = Message.getTearLinePosition(msg);
|
||||||
|
|
||||||
if(tearLinePos > -1) {
|
if(tearLinePos > -1) {
|
||||||
msg = insert(msg, tearLinePos, bodyMessageView.getSGRFor('text'));
|
msg = insert(msg, tearLinePos, bodyMessageView.getSGRFor('text'));
|
||||||
|
@ -432,7 +435,55 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} 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(
|
theme.displayThemedAsset(
|
||||||
footerArt,
|
footerArt,
|
||||||
self.client,
|
self.client,
|
||||||
{ font : self.menuConfig.font },
|
{ font : self.menuConfig.font, startRow: self.header.height + self.body.height },
|
||||||
function displayed(err, artData) {
|
function displayed(err, artData) {
|
||||||
callback(err, artData);
|
callback(err, artData);
|
||||||
}
|
}
|
||||||
|
@ -575,19 +626,34 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
|
||||||
async.series(
|
async.series(
|
||||||
[
|
[
|
||||||
function displayHeaderAndBody(callback) {
|
function displayHeaderAndBody(callback) {
|
||||||
async.eachSeries( comps, function dispArt(n, next) {
|
async.waterfall(
|
||||||
|
[
|
||||||
|
function displayHeader(callback) {
|
||||||
theme.displayThemedAsset(
|
theme.displayThemedAsset(
|
||||||
art[n],
|
art['header'],
|
||||||
self.client,
|
self.client,
|
||||||
{ font : self.menuConfig.font },
|
{ font : self.menuConfig.font },
|
||||||
function displayed(err) {
|
function displayed(err, artInfo) {
|
||||||
next(err);
|
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;
|
//self.body.height = self.client.term.termHeight - self.header.height - 1;
|
||||||
callback(err);
|
callback(err);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
function displayFooter(callback) {
|
function displayFooter(callback) {
|
||||||
// we have to treat the footer special
|
// we have to treat the footer special
|
||||||
|
@ -649,31 +715,39 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
|
||||||
|
|
||||||
assert(_.isObject(art));
|
assert(_.isObject(art));
|
||||||
|
|
||||||
async.series(
|
async.waterfall(
|
||||||
[
|
[
|
||||||
function beforeDisplayArt(callback) {
|
function beforeDisplayArt(callback) {
|
||||||
self.beforeArt(callback);
|
self.beforeArt(callback);
|
||||||
},
|
},
|
||||||
function displayHeaderAndBodyArt(callback) {
|
function displayHeader(callback) {
|
||||||
async.eachSeries( [ 'header', 'body' ], function dispArt(n, next) {
|
|
||||||
theme.displayThemedAsset(
|
theme.displayThemedAsset(
|
||||||
art[n],
|
art.header,
|
||||||
self.client,
|
self.client,
|
||||||
{ font : self.menuConfig.font },
|
{ font : self.menuConfig.font },
|
||||||
function displayed(err, artData) {
|
function displayed(err, artInfo) {
|
||||||
if(artData) {
|
if(artInfo) {
|
||||||
mciData[n] = artData;
|
mciData['header'] = artInfo;
|
||||||
self[n] = { height : artData.height };
|
self.header = {height: artInfo.height};
|
||||||
}
|
}
|
||||||
|
return callback(err, artInfo);
|
||||||
next(err);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}, 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();
|
self.setInitialFooterMode();
|
||||||
|
|
||||||
var footerName = self.getFooterName();
|
var footerName = self.getFooterName();
|
||||||
|
@ -740,10 +814,10 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
function prepareViewStates(callback) {
|
function prepareViewStates(callback) {
|
||||||
var header = self.viewControllers.header;
|
let from = self.viewControllers.header.getView(MciViewIds.header.from);
|
||||||
var from = header.getView(MciViewIds.header.from);
|
if (from) {
|
||||||
from.acceptsFocus = false;
|
from.acceptsFocus = false;
|
||||||
//from.setText(self.client.user.username);
|
}
|
||||||
|
|
||||||
// :TODO: make this a method
|
// :TODO: make this a method
|
||||||
var body = self.viewControllers.body.getView(MciViewIds.body.message);
|
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 fromView = self.viewControllers.header.getView(MciViewIds.header.from);
|
||||||
const area = getMessageAreaByTag(self.messageAreaTag);
|
const area = getMessageAreaByTag(self.messageAreaTag);
|
||||||
|
if(fromView !== undefined) {
|
||||||
if(area && area.realNames) {
|
if(area && area.realNames) {
|
||||||
fromView.setText(self.client.user.properties[UserProps.RealName] || self.client.user.username);
|
fromView.setText(self.client.user.properties[UserProps.RealName] || self.client.user.username);
|
||||||
} else {
|
} else {
|
||||||
fromView.setText(self.client.user.username);
|
fromView.setText(self.client.user.username);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if(self.replyToMessage) {
|
if(self.replyToMessage) {
|
||||||
self.initHeaderReplyEditMode();
|
self.initHeaderReplyEditMode();
|
||||||
|
@ -863,7 +939,10 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
|
||||||
}
|
}
|
||||||
|
|
||||||
initHeaderViewMode() {
|
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.from, this.message.fromUserName);
|
||||||
|
}
|
||||||
this.setHeaderText(MciViewIds.header.to, this.message.toUserName);
|
this.setHeaderText(MciViewIds.header.to, this.message.toUserName);
|
||||||
this.setHeaderText(MciViewIds.header.subject, this.message.subject);
|
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.text = this.text.substr(0, this.text.length - 1);
|
||||||
this.clientBackspace();
|
this.clientBackspace();
|
||||||
} else {
|
} else {
|
||||||
while(this.patternArrayPos > 0) {
|
while(this.patternArrayPos >= 0) {
|
||||||
if(_.isRegExp(this.patternArray[this.patternArrayPos])) {
|
if(_.isRegExp(this.patternArray[this.patternArrayPos])) {
|
||||||
this.text = this.text.substr(0, this.text.length - 1);
|
this.text = this.text.substr(0, this.text.length - 1);
|
||||||
this.client.term.write(ansi.goto(this.position.row, this.getEndOfTextColumn() + 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 ButtonView = require('./button_view.js').ButtonView;
|
||||||
const VerticalMenuView = require('./vertical_menu_view.js').VerticalMenuView;
|
const VerticalMenuView = require('./vertical_menu_view.js').VerticalMenuView;
|
||||||
const HorizontalMenuView = require('./horizontal_menu_view.js').HorizontalMenuView;
|
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 SpinnerMenuView = require('./spinner_menu_view.js').SpinnerMenuView;
|
||||||
const ToggleMenuView = require('./toggle_menu_view.js').ToggleMenuView;
|
const ToggleMenuView = require('./toggle_menu_view.js').ToggleMenuView;
|
||||||
const MaskEditTextView = require('./mask_edit_text_view.js').MaskEditTextView;
|
const MaskEditTextView = require('./mask_edit_text_view.js').MaskEditTextView;
|
||||||
|
@ -27,7 +28,7 @@ function MCIViewFactory(client) {
|
||||||
}
|
}
|
||||||
|
|
||||||
MCIViewFactory.UserViewCodes = [
|
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
|
// XY is a special MCI code that allows finding positions
|
||||||
|
@ -164,6 +165,18 @@ MCIViewFactory.prototype.createFromMCI = function(mci) {
|
||||||
view = new HorizontalMenuView(options);
|
view = new HorizontalMenuView(options);
|
||||||
break;
|
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' :
|
case 'SM' :
|
||||||
setOption(0, 'textStyle');
|
setOption(0, 'textStyle');
|
||||||
setOption(1, 'justify');
|
setOption(1, 'justify');
|
||||||
|
|
|
@ -56,14 +56,14 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
||||||
initSequence() {
|
initSequence() {
|
||||||
const self = this;
|
const self = this;
|
||||||
const mciData = {};
|
const mciData = {};
|
||||||
let pausePosition;
|
let pausePosition = {row: 0, column: 0};
|
||||||
|
|
||||||
const hasArt = () => {
|
const hasArt = () => {
|
||||||
return _.isString(self.menuConfig.art) ||
|
return _.isString(self.menuConfig.art) ||
|
||||||
(Array.isArray(self.menuConfig.art) && _.has(self.menuConfig.art[0], 'acs'));
|
(Array.isArray(self.menuConfig.art) && _.has(self.menuConfig.art[0], 'acs'));
|
||||||
};
|
};
|
||||||
|
|
||||||
async.series(
|
async.waterfall(
|
||||||
[
|
[
|
||||||
function beforeArtInterrupt(callback) {
|
function beforeArtInterrupt(callback) {
|
||||||
return self.displayQueuedInterruptions(callback);
|
return self.displayQueuedInterruptions(callback);
|
||||||
|
@ -73,7 +73,7 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
||||||
},
|
},
|
||||||
function displayMenuArt(callback) {
|
function displayMenuArt(callback) {
|
||||||
if(!hasArt()) {
|
if(!hasArt()) {
|
||||||
return callback(null);
|
return callback(null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.displayAsset(
|
self.displayAsset(
|
||||||
|
@ -86,18 +86,15 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
||||||
mciData.menu = artData.mciMap;
|
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) {
|
function displayPromptArt(artData, 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) {
|
|
||||||
if(!_.isString(self.menuConfig.prompt)) {
|
if(!_.isString(self.menuConfig.prompt)) {
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
}
|
||||||
|
@ -106,41 +103,41 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
||||||
return callback(Errors.MissingConfig('Prompt specified but no "promptConfig" block found'));
|
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.displayAsset(
|
||||||
self.menuConfig.promptConfig.art,
|
self.menuConfig.promptConfig.art,
|
||||||
self.menuConfig.config,
|
options,
|
||||||
(err, artData) => {
|
(err, artData) => {
|
||||||
if(artData) {
|
if(artData) {
|
||||||
mciData.prompt = artData.mciMap;
|
mciData.prompt = artData.mciMap;
|
||||||
|
pausePosition.row = artData.height + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return callback(err); // pass err here; prompts *must* have art
|
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) {
|
function afterArtDisplayed(callback) {
|
||||||
return self.mciReady(mciData, callback);
|
return self.mciReady(mciData, callback);
|
||||||
},
|
},
|
||||||
function displayPauseIfRequested(callback) {
|
function displayPauseIfRequested(callback) {
|
||||||
if(!self.shouldPause()) {
|
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);
|
return self.pausePrompt(pausePosition, callback);
|
||||||
},
|
},
|
||||||
function finishAndNext(callback) {
|
function finishAndNext(artInfo, callback) {
|
||||||
self.finishedLoading();
|
self.finishedLoading();
|
||||||
self.realTimeInterrupt = 'allowed';
|
self.realTimeInterrupt = 'allowed';
|
||||||
return self.autoNextMenu(callback);
|
return self.autoNextMenu(callback);
|
||||||
|
@ -512,7 +509,7 @@ exports.MenuModule = class MenuModule extends PluginModule {
|
||||||
|
|
||||||
this.optionalMoveToPosition(position);
|
this.optionalMoveToPosition(position);
|
||||||
|
|
||||||
return theme.displayThemedPause(this.client, cb);
|
return theme.displayThemedPause(this.client, {position}, cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
promptForInput( { formName, formId, promptName, prevFormName, position } = {}, options, 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.focusedItemIndex = this.items.length >= this.focusedItemIndex ? this.focusedItemIndex : 0;
|
||||||
|
|
||||||
this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0;
|
this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0;
|
||||||
|
this.itemHorizSpacing = _.isNumber(options.itemHorizSpacing) ? options.itemHorizSpacing : 0;
|
||||||
|
|
||||||
// :TODO: probably just replace this with owner draw / pipe codes / etc. more control, less specialization
|
// :TODO: probably just replace this with owner draw / pipe codes / etc. more control, less specialization
|
||||||
this.focusPrefix = options.focusPrefix || '';
|
this.focusPrefix = options.focusPrefix || '';
|
||||||
this.focusSuffix = options.focusSuffix || '';
|
this.focusSuffix = options.focusSuffix || '';
|
||||||
|
|
||||||
this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1);
|
this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1);
|
||||||
this.justify = options.justify || 'none';
|
|
||||||
|
|
||||||
this.hasFocusItems = function() {
|
this.hasFocusItems = function() {
|
||||||
return !_.isUndefined(self.focusItems);
|
return !_.isUndefined(self.focusItems);
|
||||||
|
@ -68,6 +68,15 @@ function MenuView(options) {
|
||||||
|
|
||||||
util.inherits(MenuView, View);
|
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) {
|
MenuView.prototype.setItems = function(items) {
|
||||||
if(Array.isArray(items)) {
|
if(Array.isArray(items)) {
|
||||||
this.sorted = false;
|
this.sorted = false;
|
||||||
|
@ -253,19 +262,32 @@ MenuView.prototype.setItemSpacing = function(itemSpacing) {
|
||||||
this.positionCacheExpired = true;
|
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) {
|
MenuView.prototype.setPropertyValue = function(propName, value) {
|
||||||
switch(propName) {
|
switch(propName) {
|
||||||
case 'itemSpacing' : this.setItemSpacing(value); break;
|
case 'itemSpacing' : this.setItemSpacing(value); break;
|
||||||
|
case 'itemHorizSpacing' : this.setItemHorizSpacing(value); break;
|
||||||
case 'items' : this.setItems(value); break;
|
case 'items' : this.setItems(value); break;
|
||||||
case 'focusItems' : this.setFocusItems(value); break;
|
case 'focusItems' : this.setFocusItems(value); break;
|
||||||
case 'hotKeys' : this.setHotKeys(value); break;
|
case 'hotKeys' : this.setHotKeys(value); break;
|
||||||
|
case 'textOverflow' : this.setTextOverflow(value); break;
|
||||||
case 'hotKeySubmit' : this.hotKeySubmit = 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 'focusItemIndex' : this.focusedItemIndex = value; break;
|
||||||
|
|
||||||
case 'itemFormat' :
|
case 'itemFormat' :
|
||||||
case 'focusItemFormat' :
|
case 'focusItemFormat' :
|
||||||
this[propName] = value;
|
this[propName] = value;
|
||||||
|
// if there is a cache currently, invalidate it
|
||||||
|
this.invalidateRenderCache();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'sort' : this.setSort(value); 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.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) {
|
MenuView.prototype.setHotKeys = function(hotKeys) {
|
||||||
if(_.isObject(hotKeys)) {
|
if(_.isObject(hotKeys)) {
|
||||||
if(this.caseInsensitiveHotKeys) {
|
if(this.caseInsensitiveHotKeys) {
|
||||||
|
|
|
@ -790,7 +790,7 @@ module.exports = class Message {
|
||||||
return ftnUtil.getQuotePrefix(this[source]);
|
return ftnUtil.getQuotePrefix(this[source]);
|
||||||
}
|
}
|
||||||
|
|
||||||
getTearLinePosition(input) {
|
static getTearLinePosition(input) {
|
||||||
const m = input.match(/^--- .+$(?![\s\S]*^--- .+$)/m);
|
const m = input.match(/^--- .+$(?![\s\S]*^--- .+$)/m);
|
||||||
return m ? m.index : -1;
|
return m ? m.index : -1;
|
||||||
}
|
}
|
||||||
|
@ -886,12 +886,12 @@ module.exports = class Message {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} 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 quoted = [];
|
||||||
const input = _.trimEnd(this.message).replace(/\x08/g, ''); // eslint-disable-line no-control-regex
|
const input = _.trimEnd(this.message).replace(/\x08/g, ''); // eslint-disable-line no-control-regex
|
||||||
|
|
||||||
// find *last* tearline
|
// 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
|
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 => {
|
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) {
|
if(quoted.length > 0) {
|
||||||
//
|
//
|
||||||
// Preserve paragraph seperation.
|
// Preserve paragraph separation.
|
||||||
//
|
//
|
||||||
// FSC-0032 states something about leaving blank lines fully blank
|
// FSC-0032 states something about leaving blank lines fully blank
|
||||||
// (without a prefix) but it seems nicer (and more consistent with other systems)
|
// (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]) {
|
switch (cmd[0]) {
|
||||||
case 'pm':
|
case 'pm':
|
||||||
this.processOutgoingMessage(cmd[2], cmd[1]);
|
const newmsg = cmd.slice(2).join(' ');
|
||||||
|
this.processOutgoingMessage(newmsg, cmd[1]);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'rainbow': {
|
case 'rainbow': {
|
||||||
|
|
|
@ -65,7 +65,7 @@ const _ = require('lodash');
|
||||||
const SPECIAL_KEY_MAP_DEFAULT = {
|
const SPECIAL_KEY_MAP_DEFAULT = {
|
||||||
'line feed' : [ 'return' ],
|
'line feed' : [ 'return' ],
|
||||||
exit : [ 'esc' ],
|
exit : [ 'esc' ],
|
||||||
backspace : [ 'backspace' ],
|
backspace : [ 'backspace', 'ctrl + d' ], // https://www.tecmint.com/linux-command-line-bash-shortcut-keys/
|
||||||
delete : [ 'delete' ],
|
delete : [ 'delete' ],
|
||||||
tab : [ 'tab' ],
|
tab : [ 'tab' ],
|
||||||
up : [ 'up arrow' ],
|
up : [ 'up arrow' ],
|
||||||
|
@ -74,7 +74,7 @@ const SPECIAL_KEY_MAP_DEFAULT = {
|
||||||
home : [ 'home' ],
|
home : [ 'home' ],
|
||||||
left : [ 'left arrow' ],
|
left : [ 'left arrow' ],
|
||||||
right : [ 'right 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 up' : [ 'page up' ],
|
||||||
'page down' : [ 'page down' ],
|
'page down' : [ 'page down' ],
|
||||||
insert : [ 'insert', 'ctrl + v' ],
|
insert : [ 'insert', 'ctrl + v' ],
|
||||||
|
@ -265,11 +265,10 @@ function MultiLineEditTextView(options) {
|
||||||
|
|
||||||
this.getRenderText = function(index) {
|
this.getRenderText = function(index) {
|
||||||
let text = self.getVisibleText(index);
|
let text = self.getVisibleText(index);
|
||||||
const remain = self.dimens.width - text.length;
|
const remain = self.dimens.width - strUtil.renderStringLength(text);
|
||||||
|
|
||||||
if(remain > 0) {
|
if(remain > 0) {
|
||||||
text += ' '.repeat(remain + 1);
|
text += ' '.repeat(remain);// + 1);
|
||||||
// text += new Array(remain + 1).join(' ');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return text;
|
return text;
|
||||||
|
|
|
@ -153,6 +153,8 @@ function scanFileAreaForChanges(areaInfo, options, cb) {
|
||||||
function updateTags(fe) {
|
function updateTags(fe) {
|
||||||
if(Array.isArray(options.tags)) {
|
if(Array.isArray(options.tags)) {
|
||||||
fe.hashTags = new Set(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,
|
fullPath,
|
||||||
{
|
{
|
||||||
areaTag : areaInfo.areaTag,
|
areaTag : areaInfo.areaTag,
|
||||||
storageTag : storageLoc.storageTag
|
storageTag : storageLoc.storageTag,
|
||||||
|
hashTags : areaInfo.hashTags,
|
||||||
},
|
},
|
||||||
(stepInfo, next) => {
|
(stepInfo, next) => {
|
||||||
if(argv.verbose) {
|
if(argv.verbose) {
|
||||||
|
@ -549,7 +552,7 @@ function scanFileAreas() {
|
||||||
console.info(`Processing area "${areaInfo.name}":`);
|
console.info(`Processing area "${areaInfo.name}":`);
|
||||||
|
|
||||||
scanFileAreaForChanges(areaInfo, options, err => {
|
scanFileAreaForChanges(areaInfo, options, err => {
|
||||||
return callback(err);
|
return nextAreaTag(err);
|
||||||
});
|
});
|
||||||
}, err => {
|
}, err => {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
|
|
|
@ -941,7 +941,7 @@ class QWKPacketWriter extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
// First block is a space padded ID
|
// 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.messagesStream.write(id.padEnd(QWKMessageBlockSize, ' '), 'ascii');
|
||||||
this.currentMessageOffset = QWKMessageBlockSize;
|
this.currentMessageOffset = QWKMessageBlockSize;
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,6 @@ const _ = require('lodash');
|
||||||
const fs = require('graceful-fs');
|
const fs = require('graceful-fs');
|
||||||
const paths = require('path');
|
const paths = require('path');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const async = require('async');
|
|
||||||
|
|
||||||
const ModuleInfo = exports.moduleInfo = {
|
const ModuleInfo = exports.moduleInfo = {
|
||||||
name : 'Gopher',
|
name : 'Gopher',
|
||||||
|
@ -89,7 +88,15 @@ exports.getModule = class GopherModule extends ServerModule {
|
||||||
socket.setEncoding('ascii');
|
socket.setEncoding('ascii');
|
||||||
|
|
||||||
socket.on('data', data => {
|
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);
|
this.routeRequest(data, socket);
|
||||||
|
} else {
|
||||||
|
this.notFoundGenerator('**invalid selector**', res => {
|
||||||
|
return socket.end(`${res}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('error', err => {
|
socket.on('error', err => {
|
||||||
|
|
|
@ -125,7 +125,7 @@ class NNTPServer extends NNTPServerBase {
|
||||||
const config = Config();
|
const config = Config();
|
||||||
this.groupCache = new LRU({
|
this.groupCache = new LRU({
|
||||||
max : _.get(config, 'contentServers.nntp.cache.maxItems', 200),
|
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
|
// However, as of this writing, NetRunner and SyncTERM both
|
||||||
// fail to respond to OpenSSH keep-alive pings (keepalive@openssh.com)
|
// 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) => {
|
this.server.on('connection', (conn, info) => {
|
||||||
Log.info(info, 'New SSH connection');
|
Log.info(info, 'New SSH connection');
|
||||||
this.handleNewClient(new SSHClient(conn), conn._sock, ModuleInfo);
|
this.handleNewClient(new SSHClient(conn), conn._sock, ModuleInfo);
|
||||||
|
|
|
@ -103,14 +103,16 @@ class TelnetClient {
|
||||||
case Options.NEW_ENVIRON :
|
case Options.NEW_ENVIRON :
|
||||||
{
|
{
|
||||||
this._logDebug(
|
this._logDebug(
|
||||||
{ vars : command.optionData.vars, userVars : command.optionData.userVars },
|
{ vars : command.optionData.vars, uservars : command.optionData.uservars },
|
||||||
'New environment received'
|
'New environment received'
|
||||||
);
|
);
|
||||||
|
|
||||||
// get a value from vars with fallback of user vars
|
// get a value from vars with fallback of user vars
|
||||||
const getValue = (name) => {
|
const getValue = (name) => {
|
||||||
return command.optionData.vars.find(nv => nv.name === name) ||
|
return command.optionData.vars &&
|
||||||
command.optionData.userVars.find(nv => nv.name === name);
|
(command.optionData.vars.find(nv => nv.name === name) ||
|
||||||
|
command.optionData.uservars.find(nv => nv.name === name)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if ('unknown' === this.term.termType) {
|
if ('unknown' === this.term.termType) {
|
||||||
|
|
|
@ -171,22 +171,15 @@ exports.getModule = class ShowArtModule extends MenuModule {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
const mciData = { menu : artData.mciMap };
|
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) {
|
function afterArtDisplayed(mciData, pausePosition, callback) {
|
||||||
self.mciReady(mciData, err => {
|
self.mciReady(mciData, err => {
|
||||||
return callback(err, pausePosition);
|
return callback(err, pausePosition);
|
||||||
|
|
|
@ -20,6 +20,7 @@ exports.stringFromNullTermBuffer = stringFromNullTermBuffer;
|
||||||
exports.stringToNullTermBuffer = stringToNullTermBuffer;
|
exports.stringToNullTermBuffer = stringToNullTermBuffer;
|
||||||
exports.renderSubstr = renderSubstr;
|
exports.renderSubstr = renderSubstr;
|
||||||
exports.renderStringLength = renderStringLength;
|
exports.renderStringLength = renderStringLength;
|
||||||
|
exports.ansiRenderStringLength = ansiRenderStringLength;
|
||||||
exports.formatByteSizeAbbr = formatByteSizeAbbr;
|
exports.formatByteSizeAbbr = formatByteSizeAbbr;
|
||||||
exports.formatByteSize = formatByteSize;
|
exports.formatByteSize = formatByteSize;
|
||||||
exports.formatCountAbbr = formatCountAbbr;
|
exports.formatCountAbbr = formatCountAbbr;
|
||||||
|
@ -297,7 +298,7 @@ function renderStringLength(s) {
|
||||||
let len = 0;
|
let len = 0;
|
||||||
|
|
||||||
const re = ANSI_OR_PIPE_REGEXP;
|
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
|
// Loop counting only literal (non-control) sequences
|
||||||
|
@ -312,7 +313,41 @@ function renderStringLength(s) {
|
||||||
len += s.slice(pos, m.index).length;
|
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;
|
len += parseInt(m[2], 10) || 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ exports.nextConf = nextConf;
|
||||||
exports.prevArea = prevArea;
|
exports.prevArea = prevArea;
|
||||||
exports.nextArea = nextArea;
|
exports.nextArea = nextArea;
|
||||||
exports.sendForgotPasswordEmail = sendForgotPasswordEmail;
|
exports.sendForgotPasswordEmail = sendForgotPasswordEmail;
|
||||||
|
exports.optimizeDatabases = optimizeDatabases;
|
||||||
|
|
||||||
const handleAuthFailures = (callingMenu, err, cb) => {
|
const handleAuthFailures = (callingMenu, err, cb) => {
|
||||||
// already logged in with this user?
|
// already logged in with this user?
|
||||||
|
@ -205,3 +206,25 @@ function sendForgotPasswordEmail(callingMenu, formData, extraArgs, cb) {
|
||||||
return logoff(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.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) {
|
this.drawText = function(s) {
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -125,7 +82,7 @@ function TextView(options) {
|
||||||
this.client.term.write(
|
this.client.term.write(
|
||||||
padStr(
|
padStr(
|
||||||
textToDraw,
|
textToDraw,
|
||||||
this.dimens.width + 1,
|
this.dimens.width,
|
||||||
renderedFillChar, //this.fillChar,
|
renderedFillChar, //this.fillChar,
|
||||||
this.justify,
|
this.justify,
|
||||||
this.hasFocus ? this.getFocusSGR() : this.getSGR(),
|
this.hasFocus ? this.getFocusSGR() : this.getSGR(),
|
||||||
|
|
|
@ -495,6 +495,7 @@ function displayPreparedArt(options, artInfo, cb) {
|
||||||
sauce : artInfo.sauce,
|
sauce : artInfo.sauce,
|
||||||
font : options.font,
|
font : options.font,
|
||||||
trailingLF : options.trailingLF,
|
trailingLF : options.trailingLF,
|
||||||
|
startRow : options.startRow,
|
||||||
};
|
};
|
||||||
art.display(options.client, artInfo.data, displayOpts, (err, mciMap, extraInfo) => {
|
art.display(options.client, artInfo.data, displayOpts, (err, mciMap, extraInfo) => {
|
||||||
return cb(err, { mciMap : mciMap, artInfo : artInfo, extraInfo : extraInfo } );
|
return cb(err, { mciMap : mciMap, artInfo : artInfo, extraInfo : extraInfo } );
|
||||||
|
@ -551,6 +552,7 @@ function displayThemedPrompt(name, client, options, cb) {
|
||||||
|
|
||||||
if(options.clearScreen) {
|
if(options.clearScreen) {
|
||||||
client.term.rawWrite(ansi.resetScreen());
|
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 );
|
const dispOptions = Object.assign( {}, options, promptConfig.config );
|
||||||
// :TODO: We can use term detection to do nifty things like avoid this kind of kludge:
|
// :TODO: We can use term detection to do nifty things like avoid this kind of kludge:
|
||||||
if(!options.clearScreen) {
|
// if(!options.clearScreen) {
|
||||||
dispOptions.font = 'not_really_a_font!'; // kludge :)
|
// dispOptions.font = 'not_really_a_font!'; // kludge :)
|
||||||
}
|
// }
|
||||||
|
|
||||||
displayThemedAsset(
|
displayThemedAsset(
|
||||||
promptConfig.art,
|
promptConfig.art,
|
||||||
|
@ -583,12 +585,15 @@ function displayThemedPrompt(name, client, options, cb) {
|
||||||
return callback(null, promptConfig, artInfo);
|
return callback(null, promptConfig, artInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
client.once('cursor position report', pos => {
|
if(_.isNumber(options?.position?.row)) {
|
||||||
artInfo.startRow = pos[0] - artInfo.height;
|
artInfo.startRow = options.position.row;
|
||||||
return callback(null, promptConfig, artInfo);
|
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) {
|
function createMCIViews(promptConfig, artInfo, callback) {
|
||||||
const assocViewController = usingTempViewController ? new ViewController( { client : client } ) : options.viewController;
|
const assocViewController = usingTempViewController ? new ViewController( { client : client } ) : options.viewController;
|
||||||
|
@ -614,7 +619,9 @@ function displayThemedPrompt(name, client, options, cb) {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
function clearPauseArt(artInfo, assocViewController, callback) {
|
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) {
|
if(artInfo.startRow && artInfo.height) {
|
||||||
client.term.rawWrite(ansi.goto(artInfo.startRow, 1));
|
client.term.rawWrite(ansi.goto(artInfo.startRow, 1));
|
||||||
|
|
||||||
|
|
|
@ -332,6 +332,7 @@ exports.getModule = class UploadModule extends MenuModule {
|
||||||
const scanOpts = {
|
const scanOpts = {
|
||||||
areaTag : self.areaInfo.areaTag,
|
areaTag : self.areaInfo.areaTag,
|
||||||
storageTag : self.areaInfo.storageTags[0],
|
storageTag : self.areaInfo.storageTags[0],
|
||||||
|
hashTags : self.areaInfo.hashTags,
|
||||||
};
|
};
|
||||||
|
|
||||||
function handleScanStep(stepInfo, nextScanStep) {
|
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 = {
|
const VIEW_SPECIAL_KEY_MAP_DEFAULT = {
|
||||||
accept : [ 'return' ],
|
accept : [ 'return' ],
|
||||||
exit : [ 'esc' ],
|
exit : [ 'esc' ],
|
||||||
backspace : [ 'backspace', 'del' ],
|
backspace : [ 'backspace', 'del', 'ctrl + d'], // https://www.tecmint.com/linux-command-line-bash-shortcut-keys/
|
||||||
del : [ 'del' ],
|
del : [ 'del' ],
|
||||||
next : [ 'tab' ],
|
next : [ 'tab' ],
|
||||||
up : [ 'up arrow' ],
|
up : [ 'up arrow' ],
|
||||||
|
@ -154,7 +154,7 @@ View.prototype.setHeight = function(height) {
|
||||||
|
|
||||||
View.prototype.setWidth = function(width) {
|
View.prototype.setWidth = function(width) {
|
||||||
width = parseInt(width) || 1;
|
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;
|
this.dimens.width = width;
|
||||||
};
|
};
|
||||||
|
@ -186,7 +186,7 @@ View.prototype.setPropertyValue = function(propName, value) {
|
||||||
|
|
||||||
case 'height' : this.setHeight(value); break;
|
case 'height' : this.setHeight(value); break;
|
||||||
case 'width' : this.setWidth(value); break;
|
case 'width' : this.setWidth(value); break;
|
||||||
case 'focus' : this.setFocus(value); break;
|
case 'focus' : this.setFocusProperty(value); break;
|
||||||
|
|
||||||
case 'text' :
|
case 'text' :
|
||||||
if('setText' in this) {
|
if('setText' in this) {
|
||||||
|
@ -252,10 +252,16 @@ View.prototype.redraw = function() {
|
||||||
this.client.term.write(ansi.goto(this.position.row, this.position.col));
|
this.client.term.write(ansi.goto(this.position.row, this.position.col));
|
||||||
};
|
};
|
||||||
|
|
||||||
View.prototype.setFocus = function(focused) {
|
View.prototype.setFocusProperty = function(focused) {
|
||||||
enigAssert(this.acceptsFocus, 'View does not accept focus');
|
// Either this should accept focus, or the focus should be false
|
||||||
|
enigAssert(this.acceptsFocus || !focused, 'View does not accept focus');
|
||||||
this.hasFocus = focused;
|
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();
|
this.restoreCursor();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
/* jslint node: true */
|
/* jslint node: true */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const renderStringLength = require('./string_util.js').renderStringLength;
|
const {
|
||||||
|
ansiRenderStringLength,
|
||||||
|
} = require('./string_util');
|
||||||
|
|
||||||
// deps
|
// deps
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
|
@ -28,7 +30,7 @@ function wordWrapText(text, options) {
|
||||||
|
|
||||||
//const REGEXP_GOBBLE = new RegExp(`.{0,${options.width}}`, 'g');
|
//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!
|
// sequence if present!
|
||||||
//
|
//
|
||||||
// :TODO: Need to create ansi.getMatchRegex or something - this is used all over
|
// :TODO: Need to create ansi.getMatchRegex or something - this is used all over
|
||||||
|
@ -49,7 +51,7 @@ function wordWrapText(text, options) {
|
||||||
|
|
||||||
function appendWord() {
|
function appendWord() {
|
||||||
word.match(REGEXP_GOBBLE).forEach( w => {
|
word.match(REGEXP_GOBBLE).forEach( w => {
|
||||||
renderLen = renderStringLength(w);
|
renderLen = ansiRenderStringLength(w);
|
||||||
|
|
||||||
if(result.renderLen[i] + renderLen > options.width) {
|
if(result.renderLen[i] + renderLen > options.width) {
|
||||||
if(0 === i) {
|
if(0 === i) {
|
||||||
|
@ -70,7 +72,7 @@ function wordWrapText(text, options) {
|
||||||
//
|
//
|
||||||
// * Sublime Text 3 for example considers spaces after a word
|
// * Sublime Text 3 for example considers spaces after a word
|
||||||
// part of said word. For example, "word " would be wraped
|
// 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.
|
// * Tabs in Sublime Text 3 are also treated as a word, so, e.g.
|
||||||
// "\t" may resolve to " " and must fit within the space.
|
// "\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.
|
# This will help ensure the proper Jekyll version is running.
|
||||||
# Happy Jekylling!
|
# 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.
|
# 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
|
# If you want to use GitHub Pages, remove the "gem "jekyll"" above and
|
||||||
# uncomment the line below. To upgrade, run `bundle update github-pages`.
|
# 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!
|
# If you have any plugins, put them here!
|
||||||
group :jekyll_plugins do
|
group :jekyll_plugins do
|
||||||
gem "jekyll-feed", "~> 0.6"
|
gem 'jekyll-seo-tag', '~> 2.7.1'
|
||||||
gem 'jekyll-seo-tag'
|
gem 'jekyll-theme-hacker', '~>0.2.0'
|
||||||
gem 'jekyll-theme-hacker'
|
gem 'jekyll-sitemap', '~>1.4.0'
|
||||||
gem 'jekyll-sitemap'
|
gem 'jemoji', '~>0.12.0'
|
||||||
gem 'jemoji'
|
gem 'jekyll-relative-links', '~>0.6.1'
|
||||||
|
gem 'jekyll-minifier'
|
||||||
end
|
end
|
||||||
|
|
||||||
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
|
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
|
||||||
|
|
|
@ -1,101 +1,116 @@
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
activesupport (4.2.9)
|
activesupport (7.0.1)
|
||||||
i18n (~> 0.7)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
minitest (~> 5.1)
|
i18n (>= 1.6, < 2)
|
||||||
thread_safe (~> 0.3, >= 0.3.4)
|
minitest (>= 5.1)
|
||||||
tzinfo (~> 1.1)
|
tzinfo (~> 2.0)
|
||||||
addressable (2.5.2)
|
addressable (2.8.0)
|
||||||
public_suffix (>= 2.0.2, < 4.0)
|
public_suffix (>= 2.0.2, < 5.0)
|
||||||
colorator (1.1.0)
|
colorator (1.1.0)
|
||||||
concurrent-ruby (1.0.5)
|
concurrent-ruby (1.1.9)
|
||||||
em-websocket (0.5.1)
|
cssminify2 (2.0.1)
|
||||||
|
em-websocket (0.5.3)
|
||||||
eventmachine (>= 0.12.9)
|
eventmachine (>= 0.12.9)
|
||||||
http_parser.rb (~> 0.6.0)
|
http_parser.rb (~> 0)
|
||||||
eventmachine (1.2.5)
|
eventmachine (1.2.7)
|
||||||
ffi (1.9.24)
|
execjs (2.8.1)
|
||||||
|
ffi (1.15.5)
|
||||||
forwardable-extended (2.6.0)
|
forwardable-extended (2.6.0)
|
||||||
gemoji (3.0.0)
|
gemoji (3.0.1)
|
||||||
hacker (0.0.1)
|
html-pipeline (2.14.0)
|
||||||
html-pipeline (2.7.1)
|
|
||||||
activesupport (>= 2)
|
activesupport (>= 2)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.4)
|
||||||
http_parser.rb (0.6.0)
|
htmlcompressor (0.4.0)
|
||||||
i18n (0.9.1)
|
http_parser.rb (0.8.0)
|
||||||
|
i18n (1.9.1)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
jekyll (3.7.4)
|
jekyll (4.2.1)
|
||||||
addressable (~> 2.4)
|
addressable (~> 2.4)
|
||||||
colorator (~> 1.0)
|
colorator (~> 1.0)
|
||||||
em-websocket (~> 0.5)
|
em-websocket (~> 0.5)
|
||||||
i18n (~> 0.7)
|
i18n (~> 1.0)
|
||||||
jekyll-sass-converter (~> 1.0)
|
jekyll-sass-converter (~> 2.0)
|
||||||
jekyll-watch (~> 2.0)
|
jekyll-watch (~> 2.0)
|
||||||
kramdown (~> 1.14)
|
kramdown (~> 2.3)
|
||||||
|
kramdown-parser-gfm (~> 1.0)
|
||||||
liquid (~> 4.0)
|
liquid (~> 4.0)
|
||||||
mercenary (~> 0.3.3)
|
mercenary (~> 0.4.0)
|
||||||
pathutil (~> 0.9)
|
pathutil (~> 0.9)
|
||||||
rouge (>= 1.7, < 4)
|
rouge (~> 3.0)
|
||||||
safe_yaml (~> 1.0)
|
safe_yaml (~> 1.0)
|
||||||
jekyll-feed (0.9.2)
|
terminal-table (~> 2.0)
|
||||||
jekyll (~> 3.3)
|
jekyll-minifier (0.1.10)
|
||||||
jekyll-sass-converter (1.5.1)
|
cssminify2 (~> 2.0)
|
||||||
sass (~> 3.4)
|
htmlcompressor (~> 0.4)
|
||||||
jekyll-seo-tag (2.4.0)
|
jekyll (>= 3.5)
|
||||||
jekyll (~> 3.3)
|
json-minify (~> 0.0.3)
|
||||||
jekyll-sitemap (1.1.1)
|
uglifier (~> 4.1)
|
||||||
jekyll (~> 3.3)
|
jekyll-relative-links (0.6.1)
|
||||||
jekyll-theme-hacker (0.1.0)
|
jekyll (>= 3.3, < 5.0)
|
||||||
jekyll (~> 3.5)
|
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-seo-tag (~> 2.0)
|
||||||
jekyll-watch (2.0.0)
|
jekyll-watch (2.2.1)
|
||||||
listen (~> 3.0)
|
listen (~> 3.0)
|
||||||
jemoji (0.8.1)
|
jemoji (0.12.0)
|
||||||
activesupport (~> 4.0, >= 4.2.9)
|
|
||||||
gemoji (~> 3.0)
|
gemoji (~> 3.0)
|
||||||
html-pipeline (~> 2.2)
|
html-pipeline (~> 2.2)
|
||||||
jekyll (>= 3.0)
|
jekyll (>= 3.0, < 5.0)
|
||||||
kramdown (1.16.2)
|
json (2.6.1)
|
||||||
liquid (4.0.0)
|
json-minify (0.0.3)
|
||||||
listen (3.1.5)
|
json (> 0)
|
||||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
kramdown (2.3.1)
|
||||||
rb-inotify (~> 0.9, >= 0.9.7)
|
rexml
|
||||||
ruby_dep (~> 1.2)
|
kramdown-parser-gfm (1.1.0)
|
||||||
mercenary (0.3.6)
|
kramdown (~> 2.0)
|
||||||
mini_portile2 (2.4.0)
|
liquid (4.0.3)
|
||||||
minitest (5.11.1)
|
listen (3.7.1)
|
||||||
nokogiri (1.10.8)
|
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||||
mini_portile2 (~> 2.4.0)
|
rb-inotify (~> 0.9, >= 0.9.10)
|
||||||
pathutil (0.16.1)
|
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)
|
forwardable-extended (~> 2.6)
|
||||||
public_suffix (3.0.1)
|
public_suffix (4.0.6)
|
||||||
rb-fsevent (0.10.2)
|
racc (1.6.0)
|
||||||
rb-inotify (0.9.10)
|
rb-fsevent (0.11.0)
|
||||||
ffi (>= 1.9.24, < 2)
|
rb-inotify (0.10.1)
|
||||||
rouge (3.1.0)
|
ffi (~> 1.0)
|
||||||
ruby_dep (1.5.0)
|
rexml (3.2.5)
|
||||||
safe_yaml (1.0.4)
|
rouge (3.28.0)
|
||||||
sass (3.5.5)
|
safe_yaml (1.0.5)
|
||||||
sass-listen (~> 4.0.0)
|
sassc (2.4.0)
|
||||||
sass-listen (4.0.0)
|
ffi (~> 1.9)
|
||||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
terminal-table (2.0.0)
|
||||||
rb-inotify (~> 0.9, >= 0.9.7)
|
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||||
thread_safe (0.3.6)
|
tzinfo (2.0.4)
|
||||||
tzinfo (1.2.4)
|
concurrent-ruby (~> 1.0)
|
||||||
thread_safe (~> 0.1)
|
uglifier (4.2.0)
|
||||||
|
execjs (>= 0.3.0, < 3)
|
||||||
|
unicode-display_width (1.8.0)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
ruby
|
x86_64-linux
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
hacker
|
jekyll (~> 4.2.1)
|
||||||
jekyll (~> 3.7.0)
|
jekyll-minifier
|
||||||
jekyll-feed (~> 0.6)
|
jekyll-relative-links (~> 0.6.1)
|
||||||
jekyll-seo-tag
|
jekyll-seo-tag (~> 2.7.1)
|
||||||
jekyll-sitemap
|
jekyll-sitemap (~> 1.4.0)
|
||||||
jekyll-theme-hacker
|
jekyll-theme-hacker (~> 0.2.0)
|
||||||
jemoji
|
jemoji (~> 0.12.0)
|
||||||
tzinfo-data
|
tzinfo-data
|
||||||
|
|
||||||
BUNDLED WITH
|
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
|
markdown: kramdown
|
||||||
theme: jekyll-theme-hacker
|
theme: jekyll-theme-hacker
|
||||||
plugins:
|
plugins:
|
||||||
- jekyll-feed
|
|
||||||
- jekyll-seo-tag
|
- jekyll-seo-tag
|
||||||
|
- jekyll-relative-links
|
||||||
- jekyll-sitemap
|
- jekyll-sitemap
|
||||||
- jemoji
|
- jemoji
|
||||||
|
|
||||||
baseurl: /enigma-bbs
|
baseurl: /enigma-bbs
|
||||||
|
|
||||||
|
relative_links:
|
||||||
|
enabled: true
|
||||||
|
collections: true
|
||||||
|
|
||||||
# Exclude from processing.
|
# Exclude from processing.
|
||||||
# The following items will not be processed, by default. Create a custom list
|
# The following items will not be processed, by default. Create a custom list
|
||||||
# to override the default setting.
|
# to override the default setting.
|
||||||
|
@ -28,3 +32,102 @@ exclude:
|
||||||
- vendor/gems/
|
- vendor/gems/
|
||||||
- vendor/ruby/
|
- vendor/ruby/
|
||||||
- .idea
|
- .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 |
|
| Code | Name | Description | Notes |
|
||||||
|------|----------------------|------------------|-------|
|
|------|----------------------|------------------|-------|
|
||||||
| `TL` | Text Label | Displays text | Static content |
|
| `TL` | Text Label | Displays text | Static content. See [Text View](views/text_view.md) |
|
||||||
| `ET` | Edit Text | Collect user input | Single line entry |
|
| `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 **Mask Edits** below |
|
| `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. |
|
| `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 |
|
| `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 |
|
| `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 |
|
| `HM` | Horizontal Menu | A horizontal menu | AKA a horizontal lightbar. See [Horizontal Menu](views/horizontal_menu_view.md) |
|
||||||
| `SM` | Spinner Menu | A spinner input control | Select *one* from multiple options |
|
| `FM` | Full Menu | A menu that can go both vertical and horizontal. | See [Full Menu](views/full_menu_view.md) |
|
||||||
| `TM` | Toggle Menu | A toggle menu | Commonly used for Yes/No style input |
|
| `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 |
|
| `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.
|
: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 |
|
| Directory | Description |
|
||||||
|---------------------|-----------------------------------------------------------------------------------------------------------|
|
|---------------------|-----------------------------------------------------------------------------------------------------------|
|
||||||
| `/art/general` | Non-theme art - welcome ANSI, logoff ANSI, etc. See [General Art]({{ site.baseurl }}{% link art/general.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]({{ site.baseurl }}{% link art/themes.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` | [config.hjson](config-hjson.md) system configuration.
|
||||||
| `/config/menus` | [menu.hjson](menu-hjson.md) storage.
|
| `/config/menus` | [menu.hjson](menu-hjson.md) storage.
|
||||||
| `/config/security` | SSL certificates and public/private keys.
|
| `/config/security` | SSL certificates and public/private keys.
|
||||||
| `/db` | All ENiGMA½ databases in SQLite3 format.
|
| `/db` | All ENiGMA½ databases in SQLite3 format.
|
||||||
| `/docs` | These docs ;-)
|
| `/docs` | These docs ;-)
|
||||||
| `/dropfiles` | Dropfiles created for [local doors]({{ site.baseurl }}{% link modding/local-doors.md %})
|
| `/dropfiles` | Dropfiles created for [local doors](../modding/local-doors.md)
|
||||||
| `/logs` | Logs. See [Monitoring Logs]({{ site.baseurl }}{% link troubleshooting/monitoring-logs.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
|
| `/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`
|
| `/node_modules` | External libraries required by ENiGMA½, installed when you run `npm install`
|
||||||
| `/util` | Various tools used in running/debugging ENiGMA½
|
| `/util` | Various tools used in running/debugging ENiGMA½
|
||||||
| `/www` | ENiGMA½'s built in webserver root directory
|
| `/www` | ENiGMA½'s built in webserver root directory
|
|
@ -3,7 +3,7 @@ layout: page
|
||||||
title: Email
|
title: Email
|
||||||
---
|
---
|
||||||
## Email Support
|
## 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.
|
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. |
|
| `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! |
|
| `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. |
|
| `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:
|
Example areas section:
|
||||||
|
|
||||||
|
@ -45,6 +46,7 @@ areas: {
|
||||||
name: Retro PC
|
name: Retro PC
|
||||||
desc: Oldschool PC/DOS
|
desc: Oldschool PC/DOS
|
||||||
storageTags: [ "retro_pc_dos", "retro_pc_bbs" ]
|
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...
|
do things manually, read on...
|
||||||
|
|
||||||
## Prerequisites
|
## 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.
|
* :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`.
|
* [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)
|
:information_source: Additional information in [Archivers](../configuration/archivers.md) and [File Transfer Protocols](../configuration/file-transfer-protocols.md)
|
||||||
|
|
||||||
## Config Files
|
## 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:
|
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:
|
If everything went OK:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ENiGMA½ Copyright (c) 2014-2020, Bryan Ashby
|
ENiGMA½ Copyright (c) 2014-2022, Bryan Ashby
|
||||||
_____________________ _____ ____________________ __________\_ /
|
_____________________ _____ ____________________ __________\_ /
|
||||||
\__ ____/\_ ____ \ /____/ / _____ __ \ / ______/ // /___jp!
|
\__ ____/\_ ____ \ /____/ / _____ __ \ / ______/ // /___jp!
|
||||||
// __|___// | \// |// | \// | | \// \ /___ /_____
|
// __|___// | \// |// | \// | | \// \ /___ /_____
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue