diff --git a/.eslintrc.json b/.eslintrc.json index 53bd1287..fbe0b672 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -28,5 +28,8 @@ ], "comma-dangle": 0, "no-trailing-spaces" :"warn" + }, + "parserOptions": { + "ecmaVersion": 2020 } } \ No newline at end of file diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 00000000..43e0e6cf --- /dev/null +++ b/.github/workflows/docker.yml @@ -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 \ No newline at end of file diff --git a/.github/workflows/jekyll.yml b/.github/workflows/jekyll.yml new file mode 100644 index 00000000..a17e17ef --- /dev/null +++ b/.github/workflows/jekyll.yml @@ -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 .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 + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..9eb2ef48 --- /dev/null +++ b/CONTRIBUTING.md @@ -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. diff --git a/LICENSE.TXT b/LICENSE.TXT index af51c707..6fca7184 100644 --- a/LICENSE.TXT +++ b/LICENSE.TXT @@ -1,4 +1,4 @@ -Copyright (c) 2015-2020, Bryan D. Ashby +Copyright (c) 2015-2022, Bryan D. Ashby All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/README.md b/README.md index 188f3ec0..409b8332 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ If you feel the urge to donate, [you can do so here](https://liberapay.com/NuSko ## Support * See [Discussions](https://github.com/NuSkooler/enigma-bbs/discussions) and [the issue tracker](https://github.com/NuSkooler/enigma-bbs/issues) * **Discussion on a ENiGMA BBS!** (see Boards below) -* IRC: **#enigma-bbs** on **chat.freenode.net** ([webchat](https://webchat.freenode.net/?channels=enigma-bbs)) +* IRC: **#enigma-bbs** on **irc.libera.chat:6697(TLS)** ([webchat](https://web.libera.chat/gamja/?channels=#enigma-bbs)) * FSX_ENG on [fsxNet](http://bbs.geek.nz/#fsxNet) or ARK_ENIG on [ArakNet](https://www.araknet.xyz/) available on many fine boards * Email: bryan -at- l33t.codes * [Facebook ENiGMA½ group](https://www.facebook.com/groups/enigmabbs/) @@ -64,9 +64,9 @@ ENiGMA has been tested with many terminals. However, the following are suggested * [fORCE9](http://bbs.force9.org/): (**telnet://bbs.force9.org**) * [Undercurrents](https://undercurrents.io): (**ssh://undercurrents.io**) * [PlaneT Afr0](https://planetafr0.org/): (**ssh://planetafr0.org:8889**) -* [Goblin Studio](https://goblin.strangled.net): (**ssh://goblin.strangled.net:8889**) ## Special Thanks +(in no particular order) * [Dave Stephens aka RiPuk](https://github.com/davestephens) for the awesome [ENiGMA website](https://enigma-bbs.github.io/) and [KICK ASS documentation](https://nuskooler.github.io/enigma-bbs/), code contributions, etc. * [Daniel Mecklenburg Jr.](https://github.com/codewar65) for the awesome VTX terminal and general coding talk * [M. Brutman](http://www.brutman.com/), author of [mTCP](http://www.brutman.com/mTCP/mTCP.html) (Interwebs for DOS!) @@ -81,15 +81,16 @@ ENiGMA has been tested with many terminals. However, the following are suggested * [nail/blocktronics](http://blocktronics.org/tag/nail/) for the [sickmade Xibalba logo](http://pc.textmod.es/pack/blocktronics-420/n-xbalba.ans)! * [Whazzit/blocktronics](http://blocktronics.org/tag/whazzit/) for the amazing Mayan ANSI pieces scattered about Xibalba BBS! * [Smooth](https://16colo.rs/tags/artist/smooth)/[fUEL](https://fuel.wtf/) for lots of dope art. Why not [snag a T-Shirt](https://www.redbubble.com/people/araknet/works/39126831-enigma-1-2-software-logo-design-by-smooth-of-fuel?p=t-shirt)? -* Al's Geek Lab for the [installation video](https://youtu.be/WnN-ucVi3ZU)! +* Al's Geek Lab for the [installation video](https://youtu.be/WnN-ucVi3ZU) and of course the [Back to the BBS - Part one: The return to being online](https://www.youtube.com/watch?reload=9&v=n0OwGSX2IiQ) documentary! * Alpha for the [FTN-style configuration guide](https://medium.com/@alpha_11845/setting-up-ftn-style-message-networks-with-enigma%C2%BD-bbs-709b22a1ae0d)! +* Huge shout out to [cognitivegears ](https://github.com/cognitivegears) for the various fixes, improvements, and **removing the need for cursor position reports** providing a much better terminal experience! ...and so many others! This project would be nothing without the BBS and artscene communities! ## License Released under the [BSD 2-clause](https://opensource.org/licenses/BSD-2-Clause) license: -Copyright (c) 2015-2020, Bryan D. Ashby +Copyright (c) 2015-2022, Bryan D. Ashby All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/UPGRADE.md b/UPGRADE.md index f87c2079..800e6fd2 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -28,6 +28,33 @@ npm install # or simply 'yarn' Report your issue on Xibalba BBS, hop in #enigma-bbs on FreeNode and chat, or [file a issue on GitHub](https://github.com/NuSkooler/enigma-bbs/issues). + +# 0.0.12-beta to 0.0.13-beta +* :exclamation: The SSH server's `ssh2` module has gone through a major upgrade. Existing users will need to comment out two SSH KEX algorithms from their `config.hjson` if present else clients such as NetRunner will not be able to connect over SSH. Comment out `diffie-hellman-group-exchange-sha256` and `diffie-hellman-group-exchange-sha1` +* All features and changes are backwards compatible. There are a few new configuration options in a new `term` section in the configuration. These are all optional, but include the following options in case you use them: + +```hjson +{ + + term: { + // checkUtf8Encoding requires the use of cursor position reports, which are not supported on all terminals. + // Using this with a terminal that does not support cursor position reports results in a 2 second delay + // during the connect process, but provides better autoconfiguration of utf-8 + checkUtf8Encoding: true + + + // Checking the ANSI home position also requires the use of cursor position reports, which are not + // supported on all terminals. Using this with a terminal that does not support cursor position reports + // results in a 3 second delay during the connect process, but works around positioning problems with + // non-standard terminals. + checkAnsiHomePosition: true + } +} + +``` + +In addition to these, there are also new options for `term.cp437TermList` and `term.utf8TermList`. Under most circumstances these should not need to be changed. If you want to customize these lists, more information is available in `config_default.js` + # 0.0.11-beta to 0.0.12-beta * Be aware that `master` is now mainline! This means all `git pull`'s will yield the latest version. See [WHATSNEW](WHATSNEW.md) for more information. * **BREAKING CHANGE** There is no longer a `prompt.hjson` file. Prompts are now simply part of the menu set in the `prompts` section. If you have an existing system you will need to add your `prompt.hjson` to your `menu.hjson`'s `includes` section at a minimum. Example: diff --git a/WHATSNEW.md b/WHATSNEW.md index 0ef29581..4ae304d6 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -1,8 +1,15 @@ # Whats New This document attempts to track **major** changes and additions in ENiGMA½. For details, see GitHub. +## 0.0.13-beta +* Removed terminal `cursor position reports` from most locations in the code. This should greatly increase the number of terminal programs that work with Enigma 1/2. For more information, see [Issue #222](https://github.com/NuSkooler/enigma-bbs/issues/222). This may also resolve other issues, such as [Issue #365](https://github.com/NuSkooler/enigma-bbs/issues/365), and [Issue #320](https://github.com/NuSkooler/enigma-bbs/issues/320). Anyone that previously had terminal incompatibilities please re-check and let us know! +* Bumped up the minimum [Node.js](https://nodejs.org/en/) version to V14. This will allow more expressive Javascript programming syntax with ECMAScript 2020 to improve the development experience. +* Added new configuration options for `term.checkUtf8Encoding`, `term.checkAnsiHomePostion`, `term.cp437TermList`, and `term.utf8TermList`. More information on these options is available in `UPGRADE.md` +* Many additional backward-compatible bug fixes since the first release of 0.0.12-beta. See the [project repository](https://github.com/NuSkooler/enigma-bbs) for more information. + ## 0.0.12-beta * The `master` branch has become mainline. What this means to users is `git pull` will always give you the latest and greatest. Make sure to read [Updating](./docs/admin/updating.md) and keep an eye on `WHATSNEW.md` (this file) and [UPGRADE](UPGRADE.md)! See also [ticket #276](https://github.com/NuSkooler/enigma-bbs/issues/276). +* Development now occurs against [Node.js 14 LTS](https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V14.md). * The default configuration has been moved to [config_default.js](/core/config_default.js). * A full configuration revamp has taken place. Configuration files such as `config.hjson`, `menu.hjson`, and `theme.hjson` can now utilize includes via the `includes` directive, reference 'self' sections using `@reference:` and import environment variables with `@environment`. * An explicit prompt file previously specified by `general.promptFile` in `config.hjson` is no longer necessary. Instead, this now simply part of the `prompts` section in `menu.hjson`. The default setup still creates a separate prompt HJSON file, but it is `includes`ed in `menu.hjson`. With the removal of prompts the `PromptsChanged` event will no longer be fired. @@ -16,6 +23,11 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * `./oputil user group -group` to now accepts `~group` removing the need for special handling of the "-" character. #331 * A fix has been made to clean up old `file.db` entries when a file is removed. Previously stale records could be left or even recycled into new entries. Please see [UPGRADE.md](UPGRADE.md) for details on applying this fix (look for `tables_update_2020-11-29.sql`). * The [./docs/modding/onelinerz.md](onelinerz) module can have `dbSuffix` set in it's `config` block to specify a separate DB file. For example to use as a requests list. +* Default hash tags can now be set in file areas. Simply supply an array or list of values in a file area block via `hashTags`. +* Added ability to pass an `env` value (map) to `abracadabra` doors. See [Local Doors](./docs/modding/local-doors.md]). +* `dropFileType` is now optional when launching doors with `abracadabra`. It can also be explicitly set to `none`. +* FSE in *view* mode can now stylize quote indicators. Supply `quoteStyleLevel1` in the `config` block. This can be a single string or an array of two strings (one to style the quotee's initials, the next for the '>' character, and finally the quoted text). See the `messageAreaViewPost` menu `config` block in the default `luciano_blocktronics` `theme.hjson` file for an example. An additional level style (e.g. for nested quotes) may be added in the future. +* FSE in *view* mode can now stylize tear lines and origin lines via `tearLineStyle` and `originStyle` `config` values in the same manor as `quoteStyleLevel`. ## 0.0.11-beta * Upgraded from `alpha` to `beta` -- The software is far along and mature enough at this point! @@ -122,4 +134,4 @@ submit: [ ...LOTS more! ## Pre 0.0.8-alpha -See GitHub \ No newline at end of file +See GitHub diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 3054f4e5..4353a1fe 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -575,6 +575,23 @@ // The 'msg_list' module looks for this entry by default messageAreaViewPost: { + config: { + quoteStyleLevel1: [ + "|00|11", + "|00|08", + "|00|03", + ] + tearLineStyle: [ + "|00|08", + "|00|02", + ] + originStyle: [ + "|00|08", + "|00|06", + "|00|03", + ] + } + 0: { mci: { TL1: { diff --git a/core/abracadabra.js b/core/abracadabra.js index b2e641e1..a8a72b1d 100644 --- a/core/abracadabra.js +++ b/core/abracadabra.js @@ -11,12 +11,14 @@ const { trackDoorRunBegin, trackDoorRunEnd } = require('./door_util.js'); +const Log = require('./logger').log; // deps const async = require('async'); const assert = require('assert'); const _ = require('lodash'); const paths = require('path'); +const fs = require('graceful-fs'); const activeDoorNodeInstances = {}; @@ -70,20 +72,12 @@ exports.getModule = class AbracadabraModule extends MenuModule { // :TODO: MenuModule.validateConfig(cb) -- validate config section gracefully instead of asserts! -- { key : type, key2 : type2, ... } // .. and/or EnigAssert assert(_.isString(this.config.name, 'Config \'name\' is required')); - assert(_.isString(this.config.dropFileType, 'Config \'dropFileType\' is required')); assert(_.isString(this.config.cmd, 'Config \'cmd\' is required')); this.config.nodeMax = this.config.nodeMax || 0; this.config.args = this.config.args || []; } - /* - :TODO: - * disconnecting while door is open leaves dosemu - * http://bbslink.net/sysop.php support - * Font support ala all other menus... or does this just work? - */ - incrementActiveDoorNodeInstances() { if(activeDoorNodeInstances[this.config.name]) { activeDoorNodeInstances[this.config.name] += 1; @@ -141,11 +135,15 @@ exports.getModule = class AbracadabraModule extends MenuModule { return self.doorInstance.prepare(self.config.io || 'stdio', callback); }, function generateDropfile(callback) { - const dropFileOpts = { - fileType : self.config.dropFileType, - }; + if (!self.config.dropFileType || self.config.dropFileType.toLowerCase() === 'none') { + return callback(null); + } + + self.dropFile = new DropFile( + self.client, + { fileType : self.config.dropFileType } + ); - self.dropFile = new DropFile(self.client, dropFileOpts); return self.dropFile.createFile(callback); } ], @@ -170,17 +168,30 @@ exports.getModule = class AbracadabraModule extends MenuModule { args : this.config.args, io : this.config.io || 'stdio', encoding : this.config.encoding || 'cp437', - dropFile : this.dropFile.fileName, - dropFilePath : this.dropFile.fullPath, node : this.client.node, + env : this.config.env, }; + if (this.dropFile) { + exeInfo.dropFile = this.dropFile.fileName; + exeInfo.dropFilePath = this.dropFile.fullPath; + } + const doorTracking = trackDoorRunBegin(this.client, this.config.name); this.doorInstance.run(exeInfo, () => { trackDoorRunEnd(doorTracking); this.decrementActiveDoorNodeInstances(); + // Clean up dropfile, if any + if (exeInfo.dropFilePath) { + fs.unlink(exeInfo.dropFilePath, err => { + if (err) { + Log.warn({ error : err, path : exeInfo.dropFilePath }, 'Failed to remove drop file.'); + } + }); + } + // client may have disconnected while process was active - // we're done here if so. if(!this.client.term.output) { diff --git a/core/ansi_escape_parser.js b/core/ansi_escape_parser.js index 88063bef..438f6203 100644 --- a/core/ansi_escape_parser.js +++ b/core/ansi_escape_parser.js @@ -21,8 +21,6 @@ function ANSIEscapeParser(options) { events.EventEmitter.call(this); this.column = 1; - this.row = 1; - this.scrollBack = 0; this.graphicRendition = {}; this.parseState = { @@ -36,11 +34,15 @@ function ANSIEscapeParser(options) { trailingLF : 'default', // default|omit|no|yes, ... }); + this.mciReplaceChar = miscUtil.valueWithDefault(options.mciReplaceChar, ''); this.termHeight = miscUtil.valueWithDefault(options.termHeight, 25); this.termWidth = miscUtil.valueWithDefault(options.termWidth, 80); this.trailingLF = miscUtil.valueWithDefault(options.trailingLF, 'default'); + + this.row = Math.min(options?.startRow ?? 1, this.termHeight); + self.moveCursor = function(cols, rows) { self.column += cols; self.row += rows; @@ -69,14 +71,11 @@ function ANSIEscapeParser(options) { }; self.clearScreen = function() { - // :TODO: should be doing something with row/column? + self.column = 1; + self.row = 1; self.emit('clear screen'); }; - /* - self.rowUpdated = function() { - self.emit('row update', self.row + self.scrollBack); - };*/ self.positionUpdated = function() { self.emit('position update', self.row, self.column); @@ -190,10 +189,11 @@ function ANSIEscapeParser(options) { self.emit('mci', { - mci : mciCode, - id : id ? parseInt(id, 10) : null, - args : args, - SGR : ansi.getSGRFromGraphicRendition(self.graphicRendition, true) + position : [self.row, self.column], + mci : mciCode, + id : id ? parseInt(id, 10) : null, + args : args, + SGR : ansi.getSGRFromGraphicRendition(self.graphicRendition, true) }); if(self.mciReplaceChar.length > 0) { @@ -215,6 +215,9 @@ function ANSIEscapeParser(options) { } self.reset = function(input) { + self.column = 1; + self.row = Math.min(options?.startRow ?? 1, self.termHeight); + self.parseState = { // ignore anything past EOF marker, if any buffer : input.split(String.fromCharCode(0x1a), 1)[0], diff --git a/core/ansi_term.js b/core/ansi_term.js index cac29681..6a765da7 100644 --- a/core/ansi_term.js +++ b/core/ansi_term.js @@ -309,7 +309,8 @@ const FONT_ALIAS_TO_SYNCTERM_MAP = { 'mo_soul' : 'mo_soul', 'mosoul' : 'mo_soul', - 'mO\'sOul' : 'mo_soul', + 'mo\'soul' : 'mo_soul', + 'amiga_mosoul' : 'mo_soul', 'amiga_microknight' : 'microknight', 'amiga_microknight+' : 'microknight_plus', diff --git a/core/art.js b/core/art.js index 0ff4835f..7385d08a 100644 --- a/core/art.js +++ b/core/art.js @@ -269,19 +269,16 @@ function display(client, art, options, cb) { termHeight : client.term.termHeight, termWidth : client.term.termWidth, trailingLF : options.trailingLF, + startRow : options.startRow, }); let parseComplete = false; - let cprListener; let mciMap; const mciCprQueue = []; let artHash; let mciMapFromCache; function completed() { - if(cprListener) { - client.removeListener('cursor position report', cprListener); - } if(!options.disableMciCache && !mciMapFromCache) { // cache our MCI findings... @@ -314,18 +311,6 @@ function display(client, art, options, cb) { // no cached MCI info mciMap = {}; - cprListener = function(pos) { - if(mciCprQueue.length > 0) { - mciMap[mciCprQueue.shift()].position = pos; - - if(parseComplete && 0 === mciCprQueue.length) { - return completed(); - } - } - }; - - client.on('cursor position report', cprListener); - let generatedId = 100; ansiParser.on('mci', mciInfo => { @@ -339,18 +324,16 @@ function display(client, art, options, cb) { mapEntry.focusArgs = mciInfo.args; } else { mciMap[mapKey] = { - args : mciInfo.args, - SGR : mciInfo.SGR, - code : mciInfo.mci, - id : id, + position : mciInfo.position, + args : mciInfo.args, + SGR : mciInfo.SGR, + code : mciInfo.mci, + id : id, }; if(!mciInfo.id) { ++generatedId; } - - mciCprQueue.push(mapKey); - client.term.rawWrite(ansi.queryPos()); } }); diff --git a/core/button_view.js b/core/button_view.js index edb32e12..2aebf347 100644 --- a/core/button_view.js +++ b/core/button_view.js @@ -21,7 +21,7 @@ function ButtonView(options) { util.inherits(ButtonView, TextView); ButtonView.prototype.onKeyPress = function(ch, key) { - if(this.isKeyMapped('accept', key.name) || ' ' === ch) { + if(this.isKeyMapped('accept', (key ? key.name : ch)) || ' ' === ch) { this.submitData = 'accept'; this.emit('action', 'accept'); delete this.submitData; @@ -29,16 +29,6 @@ ButtonView.prototype.onKeyPress = function(ch, key) { ButtonView.super_.prototype.onKeyPress.call(this, ch, key); } }; -/* -ButtonView.prototype.onKeyPress = function(ch, key) { - // allow space = submit - if(' ' === ch) { - this.emit('action', 'accept'); - } - - ButtonView.super_.prototype.onKeyPress.call(this, ch, key); -}; -*/ ButtonView.prototype.getData = function() { return this.submitData || null; diff --git a/core/client_term.js b/core/client_term.js index 4cbd603c..b19b4771 100644 --- a/core/client_term.js +++ b/core/client_term.js @@ -4,11 +4,12 @@ // ENiGMA½ var Log = require('./logger.js').log; var renegadeToAnsi = require('./color_codes.js').renegadeToAnsi; - +const Config = require('./config.js').get; var iconv = require('iconv-lite'); var assert = require('assert'); var _ = require('lodash'); + exports.ClientTerminal = ClientTerminal; function ClientTerminal(output) { @@ -115,7 +116,8 @@ ClientTerminal.prototype.isNixTerm = function() { return true; } - return [ 'xterm', 'linux', 'screen', 'dumb', 'rxvt', 'konsole', 'gnome', 'x11 terminal emulator' ].includes(this.termType); + const utf8TermList = Config().term.utf8TermList; + return utf8TermList.includes(this.termType); }; ClientTerminal.prototype.isANSI = function() { @@ -153,7 +155,8 @@ ClientTerminal.prototype.isANSI = function() { // linux: // * JuiceSSH (note: TERM=linux also) // - return [ 'ansi', 'pcansi', 'pc-ansi', 'ansi-bbs', 'qansi', 'scoansi', 'syncterm', 'ansi-256color' ].includes(this.termType); + const cp437TermList = Config().term.cp437TermList; + return cp437TermList.includes(this.termType); }; // :TODO: probably need to update these to convert IAC (0xff) -> IACIAC (escape it) diff --git a/core/color_codes.js b/core/color_codes.js index ff08275e..da6c8f5d 100644 --- a/core/color_codes.js +++ b/core/color_codes.js @@ -126,7 +126,7 @@ function renegadeToAnsi(s, client) { // // Converts various control codes popular in BBS packages -// to ANSI escape sequences. Additionaly supports ENiGMA style +// to ANSI escape sequences. Additionally supports ENiGMA style // MCI codes. // // Supported control code formats: @@ -134,16 +134,17 @@ function renegadeToAnsi(s, client) { // * PCBoard : @X## where the first number/char is BG color, and second is FG // * WildCat! : @##@ the same as PCBoard without the X prefix, but with a @ suffix // * WWIV : ^# -// * CNET Y-Style : 0x19## where ## is a specific set of codes -- this is the older format -// * CNET Q-style : 0x11##} where ## is a specific set of codes -- this is the newer format +// * CNET Control-Y: AKA Y-Style -- 0x19## where ## is a specific set of codes (older format) +// * CNET Control-Q: AKA Q-style -- 0x11##} where ## is a specific set of codes (newer format) // // TODO: Add Synchronet and Celerity format support // // Resources: // * http://wiki.synchro.net/custom:colors +// * https://archive.org/stream/C-Net_Pro_3.0_1994_Perspective_Software/C-Net_Pro_3.0_1994_Perspective_Software_djvu.txt // function controlCodesToAnsi(s, client) { - const RE = /(\|([A-Z0-9]{2})|\|)|(@X([0-9A-F]{2}))|(@([0-9A-F]{2})@)|(\x03[0-9]|\x03)|(\x19(c[0-9a-f]|z[0-7]|n1|f1)|\x19)|(\x11(c[0-9a-f]|z[0-7]|n1|f1)}|\x11)/g; // eslint-disable-line no-control-regex + const RE = /(\|([A-Z0-9]{2})|\|)|(@X([0-9A-F]{2}))|(@([0-9A-F]{2})@)|(\x03[0-9]|\x03)|(\x19(c[0-9a-f]|z[0-7]|n1|f1|q1)|\x19)|(\x11(c[0-9a-f]|z[0-7]|n1|f1|q1)}|\x11)/g; // eslint-disable-line no-control-regex let m; let result = ''; diff --git a/core/config_default.js b/core/config_default.js index 98aeb4af..25b14bb7 100644 --- a/core/config_default.js +++ b/core/config_default.js @@ -17,6 +17,24 @@ module.exports = () => { achievementFile : 'achievements.hjson', }, + term : { + // checkUtf8Encoding requires the use of cursor position reports, which are not supported on all terminals. + // Using this with a terminal that does not support cursor position reports results in a 2 second delay + // during the connect process, but provides better autoconfiguration of utf-8 + checkUtf8Encoding : true, + + // Checking the ANSI home position also requires the use of cursor position reports, which are not + // supported on all terminals. Using this with a terminal that does not support cursor position reports + // results in a 3 second delay during the connect process, but works around positioning problems with + // non-standard terminals. + checkAnsiHomePosition: true, + + // List of terms that should be assumed to use cp437 encoding + cp437TermList : ['ansi', 'pcansi', 'pc-ansi', 'ansi-bbs', 'qansi', 'scoansi', 'syncterm', 'ansi-256color', 'ansi-256color-rgb'], + // List of terms that should be assumed to use utf8 encoding + utf8TermList : ['xterm', 'linux', 'screen', 'dumb', 'rxvt', 'konsole', 'gnome', 'x11 terminal emulator'], + }, + users : { usernameMin : 2, usernameMax : 16, // Note that FidoNet wants 36 max @@ -166,10 +184,11 @@ module.exports = () => { 'ecdh-sha2-nistp256', 'ecdh-sha2-nistp384', 'ecdh-sha2-nistp521', - 'diffie-hellman-group-exchange-sha256', 'diffie-hellman-group14-sha1', - 'diffie-hellman-group-exchange-sha1', 'diffie-hellman-group1-sha1', + // Group exchange not currnetly supported + // 'diffie-hellman-group-exchange-sha256', + // 'diffie-hellman-group-exchange-sha1', ], cipher : [ 'aes128-ctr', @@ -492,7 +511,7 @@ module.exports = () => { }, decompress : { cmd : '7za', - args : [ 'e', '-o{extractPath}', '{archivePath}' ] // :TODO: should be 'x'? + args : [ 'e', '-y', '-o{extractPath}', '{archivePath}' ] // :TODO: should be 'x'? }, list : { cmd : '7za', @@ -501,7 +520,7 @@ module.exports = () => { }, extract : { cmd : '7za', - args : [ 'e', '-o{extractPath}', '{archivePath}', '{fileList}' ], + args : [ 'e', '-y', '-o{extractPath}', '{archivePath}', '{fileList}' ], }, }, diff --git a/core/connect.js b/core/connect.js index 64c4ea3e..59cb4da9 100644 --- a/core/connect.js +++ b/core/connect.js @@ -4,6 +4,7 @@ // ENiGMA½ const ansi = require('./ansi_term.js'); const Events = require('./events.js'); +const Config = require('./config.js').get; const { Errors } = require('./enig_error.js'); // deps @@ -18,6 +19,13 @@ function ansiDiscoverHomePosition(client, cb) { // think of home as 0,0. If this is the case, we need to offset // our positioning to accommodate for such. // + + + if( !Config().term.checkAnsiHomePosition ) { + // Skip (and assume 1,1) if the home position check is disabled. + return cb(null); + } + const done = (err) => { client.removeListener('cursor position report', cprListener); clearTimeout(giveUpTimer); @@ -68,8 +76,9 @@ function ansiAttemptDetectUTF8(client, cb) { // // We currently only do this if the term hasn't already been ID'd as a // "*nix" terminal -- that is, xterm, etc. - // - if(!client.term.isNixTerm()) { + // Also skip this check if checkUtf8Encoding is disabled in the config + + if(!client.term.isNixTerm() || !Config().term.checkUtf8Encoding) { return cb(null); } @@ -119,6 +128,8 @@ function ansiAttemptDetectUTF8(client, cb) { return giveUp(); }, 2000); + + client.once('cursor position report', cprListener); client.term.rawWrite(ansi.goHome() + ansi.queryPos()); } @@ -199,7 +210,7 @@ function displayBanner(term) { // note: intentional formatting: term.pipeWrite(` |06Connected to |02EN|10i|02GMA|10½ |06BBS version |12|VN -|06Copyright (c) 2014-2020 Bryan Ashby |14- |12http://l33t.codes/ +|06Copyright (c) 2014-2022 Bryan Ashby |14- |12http://l33t.codes/ |06Updates & source |14- |12https://github.com/NuSkooler/enigma-bbs/ |00` ); @@ -216,7 +227,6 @@ function connectEntry(client, nextMenu) { }, function discoverHomePosition(callback) { ansiDiscoverHomePosition(client, () => { - // :TODO: If CPR for home fully fails, we should bail out on the connection with an error, e.g. ANSI support required return callback(null); // we try to continue anyway }); }, diff --git a/core/edit_text_view.js b/core/edit_text_view.js index db01b9f5..05c7224d 100644 --- a/core/edit_text_view.js +++ b/core/edit_text_view.js @@ -6,27 +6,45 @@ const TextView = require('./text_view.js').TextView; const miscUtil = require('./misc_util.js'); const strUtil = require('./string_util.js'); +const VIEW_SPECIAL_KEY_MAP_DEFAULT = require('./view').VIEW_SPECIAL_KEY_MAP_DEFAULT; + // deps const _ = require('lodash'); exports.EditTextView = EditTextView; +const EDIT_TEXT_VIEW_KEY_MAP = Object.assign({}, VIEW_SPECIAL_KEY_MAP_DEFAULT, { + delete : [ 'delete', 'ctrl + d' ], // https://www.tecmint.com/linux-command-line-bash-shortcut-keys/ +}); + function EditTextView(options) { options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block'); options.resizable = false; + if(!_.isObject(options.specialKeyMap)) { + options.specialKeyMap = EDIT_TEXT_VIEW_KEY_MAP; + } + TextView.call(this, options); this.initDefaultWidth(); - this.cursorPos = { row : 0, col : 0 }; this.clientBackspace = function() { - const fillCharSGR = this.getStyleSGR(1) || this.getSGR(); - this.client.term.write(`\b${fillCharSGR}${this.fillChar}\b${this.getFocusSGR()}`); - }; + this.text = this.text.substr(0, this.text.length - 1); + + if(this.text.length >= this.dimens.width) { + this.redraw(); + } else { + this.cursorPos.col -= 1; + if(this.cursorPos.col >= 0) { + const fillCharSGR = this.getStyleSGR(1) || this.getSGR(); + this.client.term.write(`\b${fillCharSGR}${this.fillChar}\b${this.getFocusSGR()}`); + } + } + } } require('util').inherits(EditTextView, TextView); @@ -35,19 +53,16 @@ EditTextView.prototype.onKeyPress = function(ch, key) { if(key) { if(this.isKeyMapped('backspace', key.name)) { if(this.text.length > 0) { - this.text = this.text.substr(0, this.text.length - 1); - - if(this.text.length >= this.dimens.width) { - this.redraw(); - } else { - this.cursorPos.col -= 1; - if(this.cursorPos.col >= 0) { - this.clientBackspace(); - } - } + this.clientBackspace(); } return EditTextView.super_.prototype.onKeyPress.call(this, ch, key); + } else if (this.isKeyMapped('delete', key.name)) { + // Some (mostly older) terms send 'delete' for Backspace. + // if we're at the end of the line, go ahead and treat them the same + if (this.text.length > 0 && this.cursorPos.col === this.text.length) { + this.clientBackspace(); + } } else if(this.isKeyMapped('clearLine', key.name)) { this.text = ''; this.cursorPos.col = 0; diff --git a/core/file_base_area.js b/core/file_base_area.js index e9926111..57cf9cc7 100644 --- a/core/file_base_area.js +++ b/core/file_base_area.js @@ -45,7 +45,7 @@ exports.getFileAreasByTagWildcardRule = getFileAreasByTagWildcardRule; exports.getFileEntryPath = getFileEntryPath; exports.changeFileAreaWithOptions = changeFileAreaWithOptions; exports.scanFile = scanFile; -exports.scanFileAreaForChanges = scanFileAreaForChanges; +//exports.scanFileAreaForChanges = scanFileAreaForChanges; exports.getDescFromFileName = getDescFromFileName; exports.getAreaStats = getAreaStats; exports.cleanUpTempSessionItems = cleanUpTempSessionItems; @@ -139,7 +139,14 @@ function getDefaultFileAreaTag(client, disableAcsCheck) { function getFileAreaByTag(areaTag) { const areaInfo = Config().fileBase.areas[areaTag]; if(areaInfo) { - areaInfo.areaTag = areaTag; // convienence! + // normalize |hashTags| + if (_.isString(areaInfo.hashTags)) { + areaInfo.hashTags = areaInfo.hashTags.trim().split(','); + } + if (Array.isArray(areaInfo.hashTags)) { + areaInfo.hashTags = new Set(areaInfo.hashTags.map(t => t.trim())); + } + areaInfo.areaTag = areaTag; // convenience! areaInfo.storage = getAreaStorageLocations(areaInfo); return areaInfo; } @@ -794,7 +801,7 @@ function scanFile(filePath, options, iterator, cb) { stepInfo.calcHashPercent = Math.round(((stepInfo.bytesProcessed / stepInfo.byteSize) * 100)); // - // Only send 'hash_update' step update if we have a noticable percentage change in progress + // Only send 'hash_update' step update if we have a noticeable percentage change in progress // const data = bytesRead < chunkSize ? buffer.slice(0, bytesRead) : buffer; if(!iterator || stepInfo.calcHashPercent === lastCalcHashPercent) { @@ -871,90 +878,91 @@ function scanFile(filePath, options, iterator, cb) { ); } -function scanFileAreaForChanges(areaInfo, options, iterator, cb) { - if(3 === arguments.length && _.isFunction(iterator)) { - cb = iterator; - iterator = null; - } else if(2 === arguments.length && _.isFunction(options)) { - cb = options; - iterator = null; - options = {}; - } +// :TODO: this stuff needs cleaned up +// function scanFileAreaForChanges(areaInfo, options, iterator, cb) { +// if(3 === arguments.length && _.isFunction(iterator)) { +// cb = iterator; +// iterator = null; +// } else if(2 === arguments.length && _.isFunction(options)) { +// cb = options; +// iterator = null; +// options = {}; +// } - const storageLocations = getAreaStorageLocations(areaInfo); +// const storageLocations = getAreaStorageLocations(areaInfo); - async.eachSeries(storageLocations, (storageLoc, nextLocation) => { - async.series( - [ - function scanPhysFiles(callback) { - const physDir = storageLoc.dir; +// async.eachSeries(storageLocations, (storageLoc, nextLocation) => { +// async.series( +// [ +// function scanPhysFiles(callback) { +// const physDir = storageLoc.dir; - fs.readdir(physDir, (err, files) => { - if(err) { - return callback(err); - } +// fs.readdir(physDir, (err, files) => { +// if(err) { +// return callback(err); +// } - async.eachSeries(files, (fileName, nextFile) => { - const fullPath = paths.join(physDir, fileName); +// async.eachSeries(files, (fileName, nextFile) => { +// const fullPath = paths.join(physDir, fileName); - fs.stat(fullPath, (err, stats) => { - if(err) { - // :TODO: Log me! - return nextFile(null); // always try next file - } +// fs.stat(fullPath, (err, stats) => { +// if(err) { +// // :TODO: Log me! +// return nextFile(null); // always try next file +// } - if(!stats.isFile()) { - return nextFile(null); - } +// if(!stats.isFile()) { +// return nextFile(null); +// } - scanFile( - fullPath, - { - areaTag : areaInfo.areaTag, - storageTag : storageLoc.storageTag - }, - iterator, - (err, fileEntry, dupeEntries) => { - if(err) { - // :TODO: Log me!!! - return nextFile(null); // try next anyway - } +// scanFile( +// fullPath, +// { +// areaTag : areaInfo.areaTag, +// storageTag : storageLoc.storageTag +// }, +// iterator, +// (err, fileEntry, dupeEntries) => { +// if(err) { +// // :TODO: Log me!!! +// return nextFile(null); // try next anyway +// } - if(dupeEntries.length > 0) { - // :TODO: Handle duplidates -- what to do here??? - } else { - if(Array.isArray(options.tags)) { - options.tags.forEach(tag => { - fileEntry.hashTags.add(tag); - }); - } - addNewFileEntry(fileEntry, fullPath, err => { - // pass along error; we failed to insert a record in our DB or something else bad - return nextFile(err); - }); - } - } - ); - }); - }, err => { - return callback(err); - }); - }); - }, - function scanDbEntries(callback) { - // :TODO: Look @ db entries for area that were *not* processed above - return callback(null); - } - ], - err => { - return nextLocation(err); - } - ); - }, - err => { - return cb(err); - }); -} +// if(dupeEntries.length > 0) { +// // :TODO: Handle duplicates -- what to do here??? +// } else { +// if(Array.isArray(options.tags)) { +// options.tags.forEach(tag => { +// fileEntry.hashTags.add(tag); +// }); +// } +// addNewFileEntry(fileEntry, fullPath, err => { +// // pass along error; we failed to insert a record in our DB or something else bad +// return nextFile(err); +// }); +// } +// } +// ); +// }); +// }, err => { +// return callback(err); +// }); +// }); +// }, +// function scanDbEntries(callback) { +// // :TODO: Look @ db entries for area that were *not* processed above +// return callback(null); +// } +// ], +// err => { +// return nextLocation(err); +// } +// ); +// }, +// err => { +// return cb(err); +// }); +// } function getDescFromFileName(fileName) { // diff --git a/core/fse.js b/core/fse.js index ba278a01..a0d8750b 100644 --- a/core/fse.js +++ b/core/fse.js @@ -21,7 +21,10 @@ const { isAnsi, stripAnsiControlCodes, insert } = require('./string_util.js'); -const { stripMciColorCodes } = require('./color_codes.js'); +const { + stripMciColorCodes, + controlCodesToAnsi, +} = require('./color_codes.js'); const Config = require('./config.js').get; const { getAddressedToInfo } = require('./mail_util.js'); const Events = require('./events.js'); @@ -418,7 +421,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul // // Find tearline - we want to color it differently. // - const tearLinePos = this.message.getTearLinePosition(msg); + const tearLinePos = Message.getTearLinePosition(msg); if(tearLinePos > -1) { msg = insert(msg, tearLinePos, bodyMessageView.getSGRFor('text')); @@ -432,7 +435,55 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } ); } else { - bodyMessageView.setText(stripAnsiControlCodes(msg)); + msg = stripAnsiControlCodes(msg); // start clean + + const styleToArray = (style, len) => { + if (!Array.isArray(style)) { + style = [ style ]; + } + while (style.length < len) { + style.push(style[0]); + } + return style; + }; + + // + // In *View* mode, if enabled, do a little prep work so we can stylize: + // - Quote indicators + // - Tear lines + // - Origins + // + if (this.menuConfig.config.quoteStyleLevel1) { + // can be a single style to cover 'XX> TEXT' or an array to cover 'XX', '>', and TEXT + // Non-standard (as for BBSes) single > TEXT, omitting space before XX, etc. are allowed + const styleL1 = styleToArray(this.menuConfig.config.quoteStyleLevel1, 3); + + const QuoteRegex = /^([ ]?)([!-~]{0,2})>([ ]*)([^\r\n]*\r?\n)/gm; + msg = msg.replace(QuoteRegex, (m, spc1, initials, spc2, text) => { + return `${spc1}${styleL1[0]}${initials}${styleL1[1]}>${spc2}${styleL1[2]}${text}${bodyMessageView.styleSGR1}`; + }); + } + + if (this.menuConfig.config.tearLineStyle) { + // '---' and TEXT + const style = styleToArray(this.menuConfig.config.tearLineStyle, 2); + + const TearLineRegex = /^--- (.+)$(?![\s\S]*^--- .+$)/m; + msg = msg.replace(TearLineRegex, (m, text) => { + return `${style[0]}--- ${style[1]}${text}${bodyMessageView.styleSGR1}`; + }); + } + + if (this.menuConfig.config.originStyle) { + const style = styleToArray(this.menuConfig.config.originStyle, 3); + + const OriginRegex = /^([ ]{1,2})\* Origin: (.+)$/m; + msg = msg.replace(OriginRegex, (m, spc, text) => { + return `${spc}${style[0]}* ${style[1]}Origin: ${style[2]}${text}${bodyMessageView.styleSGR1}`; + }); + } + + bodyMessageView.setText(controlCodesToAnsi(msg)); } } } @@ -552,7 +603,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul theme.displayThemedAsset( footerArt, self.client, - { font : self.menuConfig.font }, + { font : self.menuConfig.font, startRow: self.header.height + self.body.height }, function displayed(err, artData) { callback(err, artData); } @@ -575,19 +626,34 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul async.series( [ function displayHeaderAndBody(callback) { - async.eachSeries( comps, function dispArt(n, next) { - theme.displayThemedAsset( - art[n], - self.client, - { font : self.menuConfig.font }, - function displayed(err) { - next(err); + async.waterfall( + [ + function displayHeader(callback) { + theme.displayThemedAsset( + art['header'], + self.client, + { font : self.menuConfig.font }, + function displayed(err, artInfo) { + return callback(err, artInfo); + } + ); + }, + function displayBody(artInfo, callback) { + theme.displayThemedAsset( + art['header'], + self.client, + { font : self.menuConfig.font, startRow: artInfo.height + 1 }, + function displayed(err, artInfo) { + return callback(err, artInfo); + } + ); } - ); - }, function complete(err) { - //self.body.height = self.client.term.termHeight - self.header.height - 1; - callback(err); - }); + ], + function complete(err) { + //self.body.height = self.client.term.termHeight - self.header.height - 1; + callback(err); + } + ); }, function displayFooter(callback) { // we have to treat the footer special @@ -649,31 +715,39 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul assert(_.isObject(art)); - async.series( + async.waterfall( [ function beforeDisplayArt(callback) { self.beforeArt(callback); }, - function displayHeaderAndBodyArt(callback) { - async.eachSeries( [ 'header', 'body' ], function dispArt(n, next) { - theme.displayThemedAsset( - art[n], - self.client, - { font : self.menuConfig.font }, - function displayed(err, artData) { - if(artData) { - mciData[n] = artData; - self[n] = { height : artData.height }; - } - - next(err); + function displayHeader(callback) { + theme.displayThemedAsset( + art.header, + self.client, + { font : self.menuConfig.font }, + function displayed(err, artInfo) { + if(artInfo) { + mciData['header'] = artInfo; + self.header = {height: artInfo.height}; } - ); - }, function complete(err) { - callback(err); - }); + return callback(err, artInfo); + } + ); }, - function displayFooter(callback) { + function displayBody(artInfo, callback) { + theme.displayThemedAsset( + art.body, + self.client, + { font : self.menuConfig.font, startRow: artInfo.height + 1 }, + function displayed(err, artInfo) { + if(artInfo) { + mciData['body'] = artInfo; + self.body = {height: artInfo.height - self.header.height}; + } + return callback(err, artInfo); + }); + }, + function displayFooter(artInfo, callback) { self.setInitialFooterMode(); var footerName = self.getFooterName(); @@ -740,10 +814,10 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul }); }, function prepareViewStates(callback) { - var header = self.viewControllers.header; - var from = header.getView(MciViewIds.header.from); - from.acceptsFocus = false; - //from.setText(self.client.user.username); + let from = self.viewControllers.header.getView(MciViewIds.header.from); + if (from) { + from.acceptsFocus = false; + } // :TODO: make this a method var body = self.viewControllers.body.getView(MciViewIds.body.message); @@ -774,10 +848,12 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul { const fromView = self.viewControllers.header.getView(MciViewIds.header.from); const area = getMessageAreaByTag(self.messageAreaTag); - if(area && area.realNames) { - fromView.setText(self.client.user.properties[UserProps.RealName] || self.client.user.username); - } else { - fromView.setText(self.client.user.username); + if(fromView !== undefined) { + if(area && area.realNames) { + fromView.setText(self.client.user.properties[UserProps.RealName] || self.client.user.username); + } else { + fromView.setText(self.client.user.username); + } } if(self.replyToMessage) { @@ -863,7 +939,10 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } initHeaderViewMode() { - this.setHeaderText(MciViewIds.header.from, this.message.fromUserName); + // Only set header text for from view if it is on the form + if (this.viewControllers.header.getView(MciViewIds.header.from) !== undefined) { + this.setHeaderText(MciViewIds.header.from, this.message.fromUserName); + } this.setHeaderText(MciViewIds.header.to, this.message.toUserName); this.setHeaderText(MciViewIds.header.subject, this.message.subject); diff --git a/core/full_menu_view.js b/core/full_menu_view.js new file mode 100644 index 00000000..212b4d15 --- /dev/null +++ b/core/full_menu_view.js @@ -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; +}; diff --git a/core/mask_edit_text_view.js b/core/mask_edit_text_view.js index abd04cb1..c9163273 100644 --- a/core/mask_edit_text_view.js +++ b/core/mask_edit_text_view.js @@ -132,7 +132,7 @@ MaskEditTextView.prototype.onKeyPress = function(ch, key) { this.text = this.text.substr(0, this.text.length - 1); this.clientBackspace(); } else { - while(this.patternArrayPos > 0) { + while(this.patternArrayPos >= 0) { if(_.isRegExp(this.patternArray[this.patternArrayPos])) { this.text = this.text.substr(0, this.text.length - 1); this.client.term.write(ansi.goto(this.position.row, this.getEndOfTextColumn() + 1)); diff --git a/core/mci_view_factory.js b/core/mci_view_factory.js index 037121e5..d6c37865 100644 --- a/core/mci_view_factory.js +++ b/core/mci_view_factory.js @@ -8,6 +8,7 @@ const EditTextView = require('./edit_text_view.js').EditTextView; const ButtonView = require('./button_view.js').ButtonView; const VerticalMenuView = require('./vertical_menu_view.js').VerticalMenuView; const HorizontalMenuView = require('./horizontal_menu_view.js').HorizontalMenuView; +const FullMenuView = require('./full_menu_view.js').FullMenuView; const SpinnerMenuView = require('./spinner_menu_view.js').SpinnerMenuView; const ToggleMenuView = require('./toggle_menu_view.js').ToggleMenuView; const MaskEditTextView = require('./mask_edit_text_view.js').MaskEditTextView; @@ -27,7 +28,7 @@ function MCIViewFactory(client) { } MCIViewFactory.UserViewCodes = [ - 'TL', 'ET', 'ME', 'MT', 'PL', 'BT', 'VM', 'HM', 'SM', 'TM', 'KE', + 'TL', 'ET', 'ME', 'MT', 'PL', 'BT', 'VM', 'HM', 'FM', 'SM', 'TM', 'KE', // // XY is a special MCI code that allows finding positions @@ -164,6 +165,18 @@ MCIViewFactory.prototype.createFromMCI = function(mci) { view = new HorizontalMenuView(options); break; + // Full Menu + case 'FM' : + setOption(0, 'itemSpacing'); + setOption(1, 'itemHorizSpacing'); + setOption(2, 'justify'); + setOption(3, 'textStyle'); + + setFocusOption(0, 'focusTextStyle'); + + view = new FullMenuView(options); + break; + case 'SM' : setOption(0, 'textStyle'); setOption(1, 'justify'); diff --git a/core/menu_module.js b/core/menu_module.js index 8d325378..64fd56b5 100644 --- a/core/menu_module.js +++ b/core/menu_module.js @@ -56,14 +56,14 @@ exports.MenuModule = class MenuModule extends PluginModule { initSequence() { const self = this; const mciData = {}; - let pausePosition; + let pausePosition = {row: 0, column: 0}; const hasArt = () => { return _.isString(self.menuConfig.art) || (Array.isArray(self.menuConfig.art) && _.has(self.menuConfig.art[0], 'acs')); }; - async.series( + async.waterfall( [ function beforeArtInterrupt(callback) { return self.displayQueuedInterruptions(callback); @@ -73,7 +73,7 @@ exports.MenuModule = class MenuModule extends PluginModule { }, function displayMenuArt(callback) { if(!hasArt()) { - return callback(null); + return callback(null, null); } self.displayAsset( @@ -86,18 +86,15 @@ exports.MenuModule = class MenuModule extends PluginModule { mciData.menu = artData.mciMap; } - return callback(null); // any errors are non-fatal + if(artData) { + pausePosition.row = artData.height + 1; + } + + return callback(null, artData); // any errors are non-fatal } ); }, - function moveToPromptLocation(callback) { - if(self.menuConfig.prompt) { - // :TODO: fetch and move cursor to prompt location, if supplied. See notes/etc. on placements - } - - return callback(null); - }, - function displayPromptArt(callback) { + function displayPromptArt(artData, callback) { if(!_.isString(self.menuConfig.prompt)) { return callback(null); } @@ -106,41 +103,41 @@ exports.MenuModule = class MenuModule extends PluginModule { return callback(Errors.MissingConfig('Prompt specified but no "promptConfig" block found')); } + const options = Object.assign({}, self.menuConfig.config); + + if(_.isNumber(artData?.height)) { + options.startRow = artData.height + 1; + } + self.displayAsset( self.menuConfig.promptConfig.art, - self.menuConfig.config, + options, (err, artData) => { if(artData) { mciData.prompt = artData.mciMap; + pausePosition.row = artData.height + 1; } + return callback(err); // pass err here; prompts *must* have art } ); }, - function recordCursorPosition(callback) { - if(!self.shouldPause()) { - return callback(null); // cursor position not needed - } - - self.client.once('cursor position report', pos => { - pausePosition = { row : pos[0], col : 1 }; - self.client.log.trace('After art position recorded', pausePosition ); - return callback(null); - }); - - self.client.term.rawWrite(ansi.queryPos()); - }, function afterArtDisplayed(callback) { return self.mciReady(mciData, callback); }, function displayPauseIfRequested(callback) { if(!self.shouldPause()) { - return callback(null); + return callback(null, null); + } + + if(self.client.term.termHeight > 0 && pausePosition.row > self.client.termHeight) { + // If this scrolled, the prompt will go to the bottom of the screen + pausePosition.row = self.client.termHeight; } return self.pausePrompt(pausePosition, callback); }, - function finishAndNext(callback) { + function finishAndNext(artInfo, callback) { self.finishedLoading(); self.realTimeInterrupt = 'allowed'; return self.autoNextMenu(callback); @@ -512,7 +509,7 @@ exports.MenuModule = class MenuModule extends PluginModule { this.optionalMoveToPosition(position); - return theme.displayThemedPause(this.client, cb); + return theme.displayThemedPause(this.client, {position}, cb); } promptForInput( { formName, formId, promptName, prevFormName, position } = {}, options, cb) { diff --git a/core/menu_view.js b/core/menu_view.js index d9016153..9c750aba 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -38,14 +38,14 @@ function MenuView(options) { this.focusedItemIndex = options.focusedItemIndex || 0; this.focusedItemIndex = this.items.length >= this.focusedItemIndex ? this.focusedItemIndex : 0; - this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0; + this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0; + this.itemHorizSpacing = _.isNumber(options.itemHorizSpacing) ? options.itemHorizSpacing : 0; // :TODO: probably just replace this with owner draw / pipe codes / etc. more control, less specialization this.focusPrefix = options.focusPrefix || ''; this.focusSuffix = options.focusSuffix || ''; this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1); - this.justify = options.justify || 'none'; this.hasFocusItems = function() { return !_.isUndefined(self.focusItems); @@ -68,6 +68,15 @@ function MenuView(options) { util.inherits(MenuView, View); +MenuView.prototype.setTextOverflow = function(overflow) { + this.textOverflow = overflow; + this.invalidateRenderCache(); +} + +MenuView.prototype.hasTextOverflow = function() { + return this.textOverflow != undefined; +} + MenuView.prototype.setItems = function(items) { if(Array.isArray(items)) { this.sorted = false; @@ -253,19 +262,32 @@ MenuView.prototype.setItemSpacing = function(itemSpacing) { this.positionCacheExpired = true; }; +MenuView.prototype.setItemHorizSpacing = function(itemHorizSpacing) { + itemHorizSpacing = parseInt(itemHorizSpacing); + assert(_.isNumber(itemHorizSpacing)); + + this.itemHorizSpacing = itemHorizSpacing; + this.positionCacheExpired = true; +}; + MenuView.prototype.setPropertyValue = function(propName, value) { switch(propName) { case 'itemSpacing' : this.setItemSpacing(value); break; + case 'itemHorizSpacing' : this.setItemHorizSpacing(value); break; case 'items' : this.setItems(value); break; case 'focusItems' : this.setFocusItems(value); break; case 'hotKeys' : this.setHotKeys(value); break; + case 'textOverflow' : this.setTextOverflow(value); break; case 'hotKeySubmit' : this.hotKeySubmit = value; break; - case 'justify' : this.justify = value; break; + case 'justify' : this.setJustify(value); break; + case 'fillChar' : this.setFillChar(value); break; case 'focusItemIndex' : this.focusedItemIndex = value; break; case 'itemFormat' : case 'focusItemFormat' : this[propName] = value; + // if there is a cache currently, invalidate it + this.invalidateRenderCache(); break; case 'sort' : this.setSort(value); break; @@ -274,6 +296,17 @@ MenuView.prototype.setPropertyValue = function(propName, value) { MenuView.super_.prototype.setPropertyValue.call(this, propName, value); }; +MenuView.prototype.setFillChar = function(fillChar) { + this.fillChar = miscUtil.valueWithDefault(fillChar, ' ').substr(0, 1); + this.invalidateRenderCache(); +} + +MenuView.prototype.setJustify = function(justify) { + this.justify = justify; + this.invalidateRenderCache(); + this.positionCacheExpired = true; +} + MenuView.prototype.setHotKeys = function(hotKeys) { if(_.isObject(hotKeys)) { if(this.caseInsensitiveHotKeys) { diff --git a/core/message.js b/core/message.js index c5ad490b..e98baec2 100644 --- a/core/message.js +++ b/core/message.js @@ -790,7 +790,7 @@ module.exports = class Message { return ftnUtil.getQuotePrefix(this[source]); } - getTearLinePosition(input) { + static getTearLinePosition(input) { const m = input.match(/^--- .+$(?![\s\S]*^--- .+$)/m); return m ? m.index : -1; } @@ -886,12 +886,12 @@ module.exports = class Message { } ); } else { - const QUOTE_RE = /^ ((?:[A-Za-z0-9]{2}> )+(?:[A-Za-z0-9]{2}>)*) */; + const QUOTE_RE = /^ ((?:[A-Za-z0-9]{1,2}> )+(?:[A-Za-z0-9]{1,2}>)*) */; const quoted = []; const input = _.trimEnd(this.message).replace(/\x08/g, ''); // eslint-disable-line no-control-regex // find *last* tearline - let tearLinePos = this.getTearLinePosition(input); + let tearLinePos = Message.getTearLinePosition(input); tearLinePos = -1 === tearLinePos ? input.length : tearLinePos; // we just want the index or the entire string input.slice(0, tearLinePos).split(/\r\n\r\n|\n\n/).forEach(paragraph => { @@ -910,7 +910,7 @@ module.exports = class Message { if(quoted.length > 0) { // - // Preserve paragraph seperation. + // Preserve paragraph separation. // // FSC-0032 states something about leaving blank lines fully blank // (without a prefix) but it seems nicer (and more consistent with other systems) diff --git a/core/mrc.js b/core/mrc.js index 1e0791dd..28e0e3c3 100644 --- a/core/mrc.js +++ b/core/mrc.js @@ -426,7 +426,8 @@ exports.getModule = class mrcModule extends MenuModule { switch (cmd[0]) { case 'pm': - this.processOutgoingMessage(cmd[2], cmd[1]); + const newmsg = cmd.slice(2).join(' '); + this.processOutgoingMessage(newmsg, cmd[1]); break; case 'rainbow': { diff --git a/core/multi_line_edit_text_view.js b/core/multi_line_edit_text_view.js index 9f099b16..ce6cf10b 100644 --- a/core/multi_line_edit_text_view.js +++ b/core/multi_line_edit_text_view.js @@ -65,7 +65,7 @@ const _ = require('lodash'); const SPECIAL_KEY_MAP_DEFAULT = { 'line feed' : [ 'return' ], exit : [ 'esc' ], - backspace : [ 'backspace' ], + backspace : [ 'backspace', 'ctrl + d' ], // https://www.tecmint.com/linux-command-line-bash-shortcut-keys/ delete : [ 'delete' ], tab : [ 'tab' ], up : [ 'up arrow' ], @@ -74,7 +74,7 @@ const SPECIAL_KEY_MAP_DEFAULT = { home : [ 'home' ], left : [ 'left arrow' ], right : [ 'right arrow' ], - 'delete line' : [ 'ctrl + y' ], + 'delete line' : [ 'ctrl + y', 'ctrl + u' ], // https://en.wikipedia.org/wiki/Backspace 'page up' : [ 'page up' ], 'page down' : [ 'page down' ], insert : [ 'insert', 'ctrl + v' ], @@ -265,11 +265,10 @@ function MultiLineEditTextView(options) { this.getRenderText = function(index) { let text = self.getVisibleText(index); - const remain = self.dimens.width - text.length; + const remain = self.dimens.width - strUtil.renderStringLength(text); if(remain > 0) { - text += ' '.repeat(remain + 1); - // text += new Array(remain + 1).join(' '); + text += ' '.repeat(remain);// + 1); } return text; diff --git a/core/oputil/oputil_file_base.js b/core/oputil/oputil_file_base.js index 308d3581..5531695d 100644 --- a/core/oputil/oputil_file_base.js +++ b/core/oputil/oputil_file_base.js @@ -153,6 +153,8 @@ function scanFileAreaForChanges(areaInfo, options, cb) { function updateTags(fe) { if(Array.isArray(options.tags)) { fe.hashTags = new Set(options.tags); + } else if (areaInfo.hashTags) { // no explicit tags; merge in defaults, if any + fe.hashTags = areaInfo.hashTags; } } @@ -227,7 +229,8 @@ function scanFileAreaForChanges(areaInfo, options, cb) { fullPath, { areaTag : areaInfo.areaTag, - storageTag : storageLoc.storageTag + storageTag : storageLoc.storageTag, + hashTags : areaInfo.hashTags, }, (stepInfo, next) => { if(argv.verbose) { @@ -549,7 +552,7 @@ function scanFileAreas() { console.info(`Processing area "${areaInfo.name}":`); scanFileAreaForChanges(areaInfo, options, err => { - return callback(err); + return nextAreaTag(err); }); }, err => { return callback(err); diff --git a/core/qwk_mail_packet.js b/core/qwk_mail_packet.js index eab57eb7..26d7bef2 100644 --- a/core/qwk_mail_packet.js +++ b/core/qwk_mail_packet.js @@ -941,7 +941,7 @@ class QWKPacketWriter extends EventEmitter { } // First block is a space padded ID - const id = `Created with ENiGMA 1/2 BBS v${enigmaVersion} Copyright (c) 2015-2020 Bryan Ashby`; + const id = `Created with ENiGMA 1/2 BBS v${enigmaVersion} Copyright (c) 2015-2022 Bryan Ashby`; this.messagesStream.write(id.padEnd(QWKMessageBlockSize, ' '), 'ascii'); this.currentMessageOffset = QWKMessageBlockSize; diff --git a/core/servers/content/gopher.js b/core/servers/content/gopher.js index 442ecdcb..a817bd22 100644 --- a/core/servers/content/gopher.js +++ b/core/servers/content/gopher.js @@ -27,7 +27,6 @@ const _ = require('lodash'); const fs = require('graceful-fs'); const paths = require('path'); const moment = require('moment'); -const async = require('async'); const ModuleInfo = exports.moduleInfo = { name : 'Gopher', @@ -89,7 +88,15 @@ exports.getModule = class GopherModule extends ServerModule { socket.setEncoding('ascii'); socket.on('data', data => { - this.routeRequest(data, socket); + // sanitize a bit - bots like to inject garbage + data = data.replace(/[^ -~\t\r\n]/g, ''); + if (data) { + this.routeRequest(data, socket); + } else { + this.notFoundGenerator('**invalid selector**', res => { + return socket.end(`${res}`); + }); + } }); socket.on('error', err => { diff --git a/core/servers/content/nntp.js b/core/servers/content/nntp.js index 237c0994..7c1a263b 100644 --- a/core/servers/content/nntp.js +++ b/core/servers/content/nntp.js @@ -125,7 +125,7 @@ class NNTPServer extends NNTPServerBase { const config = Config(); this.groupCache = new LRU({ max : _.get(config, 'contentServers.nntp.cache.maxItems', 200), - maxAge : _.get(config, 'contentServers.nntp.cache.maxAge', 1000 * 30), // default=30s + ttl : _.get(config, 'contentServers.nntp.cache.maxAge', 1000 * 30), // default=30s }); } diff --git a/core/servers/login/ssh.js b/core/servers/login/ssh.js index ddb1ebfd..d1d8f19b 100644 --- a/core/servers/login/ssh.js +++ b/core/servers/login/ssh.js @@ -357,9 +357,11 @@ exports.getModule = class SSHServerModule extends LoginServerModule { // However, as of this writing, NetRunner and SyncTERM both // fail to respond to OpenSSH keep-alive pings (keepalive@openssh.com) // - ssh2.Server.KEEPALIVE_INTERVAL = 0; + // See also #399 + // + ssh2.Server.KEEPALIVE_CLIENT_INTERVAL = 0; - this.server = ssh2.Server(serverConf); + this.server = new ssh2.Server(serverConf); this.server.on('connection', (conn, info) => { Log.info(info, 'New SSH connection'); this.handleNewClient(new SSHClient(conn), conn._sock, ModuleInfo); diff --git a/core/servers/login/telnet.js b/core/servers/login/telnet.js index 12a3ed36..b3e13a8b 100644 --- a/core/servers/login/telnet.js +++ b/core/servers/login/telnet.js @@ -103,14 +103,16 @@ class TelnetClient { case Options.NEW_ENVIRON : { this._logDebug( - { vars : command.optionData.vars, userVars : command.optionData.userVars }, + { vars : command.optionData.vars, uservars : command.optionData.uservars }, 'New environment received' ); // get a value from vars with fallback of user vars const getValue = (name) => { - return command.optionData.vars.find(nv => nv.name === name) || - command.optionData.userVars.find(nv => nv.name === name); + return command.optionData.vars && + (command.optionData.vars.find(nv => nv.name === name) || + command.optionData.uservars.find(nv => nv.name === name) + ); }; if ('unknown' === this.term.termType) { diff --git a/core/show_art.js b/core/show_art.js index 7e53ca60..4236760c 100644 --- a/core/show_art.js +++ b/core/show_art.js @@ -171,22 +171,15 @@ exports.getModule = class ShowArtModule extends MenuModule { return callback(err); } const mciData = { menu : artData.mciMap }; - return callback(null, mciData); + if(self.client.term.termHeight > 0 && artData.height > self.client.term.termHeight) { + // We must have scrolled, adjust the positioning for pause + artData.height = self.client.term.termHeight; + } + const pausePosition = { row: artData.height + 1, col: 1}; + return callback(null, mciData, pausePosition); } ); }, - function recordCursorPosition(mciData, callback) { - if(!options.pause) { - return callback(null, mciData, null); // cursor position not needed - } - - self.client.once('cursor position report', pos => { - const pausePosition = { row : pos[0], col : 1 }; - return callback(null, mciData, pausePosition); - }); - - self.client.term.rawWrite(ANSI.queryPos()); - }, function afterArtDisplayed(mciData, pausePosition, callback) { self.mciReady(mciData, err => { return callback(err, pausePosition); diff --git a/core/string_util.js b/core/string_util.js index 6b88ec40..673a128f 100644 --- a/core/string_util.js +++ b/core/string_util.js @@ -20,6 +20,7 @@ exports.stringFromNullTermBuffer = stringFromNullTermBuffer; exports.stringToNullTermBuffer = stringToNullTermBuffer; exports.renderSubstr = renderSubstr; exports.renderStringLength = renderStringLength; +exports.ansiRenderStringLength = ansiRenderStringLength; exports.formatByteSizeAbbr = formatByteSizeAbbr; exports.formatByteSize = formatByteSize; exports.formatCountAbbr = formatCountAbbr; @@ -297,7 +298,7 @@ function renderStringLength(s) { let len = 0; const re = ANSI_OR_PIPE_REGEXP; - re.lastIndex = 0; // we recycle the rege; reset + re.lastIndex = 0; // we recycle the regex; reset // // Loop counting only literal (non-control) sequences @@ -312,7 +313,41 @@ function renderStringLength(s) { len += s.slice(pos, m.index).length; } - if('C' === m[3]) { // ESC[C is foward/right + if('C' === m[3]) { // ESC[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[C which means forward + // + 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[C is forward/right len += parseInt(m[2], 10) || 0; } } diff --git a/core/system_menu_method.js b/core/system_menu_method.js index ba9bb699..75e14dae 100644 --- a/core/system_menu_method.js +++ b/core/system_menu_method.js @@ -26,6 +26,7 @@ exports.nextConf = nextConf; exports.prevArea = prevArea; exports.nextArea = nextArea; exports.sendForgotPasswordEmail = sendForgotPasswordEmail; +exports.optimizeDatabases = optimizeDatabases; const handleAuthFailures = (callingMenu, err, cb) => { // already logged in with this user? @@ -205,3 +206,25 @@ function sendForgotPasswordEmail(callingMenu, formData, extraArgs, cb) { return logoff(callingMenu, formData, extraArgs, cb); }); } + +function optimizeDatabases(callingMenu, formData, extraArgs, cb) { + const dbs = require('./database').dbs; + const client = callingMenu.client; + + client.term.write('\r\n\r\n'); + + Object.keys(dbs).forEach(dbName => { + client.log.info({ dbName }, 'Optimizing database'); + + client.term.write(`Optimizing ${dbName}. Please wait...\r\n`); + + // https://www.sqlite.org/pragma.html#pragma_optimize + dbs[dbName].run('PRAGMA optimize;', err => { + if (err) { + client.log.error({ error : err, dbName }, 'Error attempting to optimize database'); + } + }); + }); + + return callingMenu.prevMenu(cb); +} \ No newline at end of file diff --git a/core/text_view.js b/core/text_view.js index 2a5c93c5..cbecb54f 100644 --- a/core/text_view.js +++ b/core/text_view.js @@ -44,49 +44,6 @@ function TextView(options) { this.textMaskChar = options.textMaskChar; } - /* - this.drawText = function(s) { - - // - // |<- this.maxLength - // ABCDEFGHIJK - // |ABCDEFG| ^_ this.text.length - // ^-- this.dimens.width - // - let textToDraw = _.isString(this.textMaskChar) ? - new Array(s.length + 1).join(this.textMaskChar) : - stylizeString(s, this.hasFocus ? this.focusTextStyle : this.textStyle); - - if(textToDraw.length > this.dimens.width) { - if(this.hasFocus) { - if(this.horizScroll) { - textToDraw = textToDraw.substr(textToDraw.length - this.dimens.width, textToDraw.length); - } - } else { - if(textToDraw.length > this.dimens.width) { - if(this.textOverflow && - this.dimens.width > this.textOverflow.length && - textToDraw.length - this.textOverflow.length >= this.textOverflow.length) - { - textToDraw = textToDraw.substr(0, this.dimens.width - this.textOverflow.length) + this.textOverflow; - } else { - textToDraw = textToDraw.substr(0, this.dimens.width); - } - } - } - } - - this.client.term.write(padStr( - textToDraw, - this.dimens.width + 1, - this.fillChar, - this.justify, - this.hasFocus ? this.getFocusSGR() : this.getSGR(), - this.getStyleSGR(1) || this.getSGR() - ), false); - }; -*/ - this.drawText = function(s) { // @@ -125,7 +82,7 @@ function TextView(options) { this.client.term.write( padStr( textToDraw, - this.dimens.width + 1, + this.dimens.width, renderedFillChar, //this.fillChar, this.justify, this.hasFocus ? this.getFocusSGR() : this.getSGR(), diff --git a/core/theme.js b/core/theme.js index f9cf5792..a511ba33 100644 --- a/core/theme.js +++ b/core/theme.js @@ -495,6 +495,7 @@ function displayPreparedArt(options, artInfo, cb) { sauce : artInfo.sauce, font : options.font, trailingLF : options.trailingLF, + startRow : options.startRow, }; art.display(options.client, artInfo.data, displayOpts, (err, mciMap, extraInfo) => { return cb(err, { mciMap : mciMap, artInfo : artInfo, extraInfo : extraInfo } ); @@ -551,6 +552,7 @@ function displayThemedPrompt(name, client, options, cb) { if(options.clearScreen) { client.term.rawWrite(ansi.resetScreen()); + options.position = {row: 1, column: 1}; } // @@ -560,9 +562,9 @@ function displayThemedPrompt(name, client, options, cb) { // const dispOptions = Object.assign( {}, options, promptConfig.config ); // :TODO: We can use term detection to do nifty things like avoid this kind of kludge: - if(!options.clearScreen) { - dispOptions.font = 'not_really_a_font!'; // kludge :) - } + // if(!options.clearScreen) { + // dispOptions.font = 'not_really_a_font!'; // kludge :) + // } displayThemedAsset( promptConfig.art, @@ -583,12 +585,15 @@ function displayThemedPrompt(name, client, options, cb) { return callback(null, promptConfig, artInfo); } - client.once('cursor position report', pos => { - artInfo.startRow = pos[0] - artInfo.height; - return callback(null, promptConfig, artInfo); - }); + if(_.isNumber(options?.position?.row)) { + artInfo.startRow = options.position.row; + if(client.term.termHeight > 0 && artInfo.startRow + artInfo.height > client.term.termHeight) { + // in this case, we will have scrolled + artInfo.startRow = client.term.termHeight - artInfo.height; + } + } - client.term.rawWrite(ansi.queryPos()); + return callback(null, promptConfig, artInfo); }, function createMCIViews(promptConfig, artInfo, callback) { const assocViewController = usingTempViewController ? new ViewController( { client : client } ) : options.viewController; @@ -614,7 +619,9 @@ function displayThemedPrompt(name, client, options, cb) { }); }, function clearPauseArt(artInfo, assocViewController, callback) { - if(options.clearPrompt) { + // Only clear with height if clearPrompt is true and if we were able + // to determine the row + if(options.clearPrompt && artInfo.startRow) { if(artInfo.startRow && artInfo.height) { client.term.rawWrite(ansi.goto(artInfo.startRow, 1)); diff --git a/core/upload.js b/core/upload.js index b451ac9a..7ab79c48 100644 --- a/core/upload.js +++ b/core/upload.js @@ -332,6 +332,7 @@ exports.getModule = class UploadModule extends MenuModule { const scanOpts = { areaTag : self.areaInfo.areaTag, storageTag : self.areaInfo.storageTags[0], + hashTags : self.areaInfo.hashTags, }; function handleScanStep(stepInfo, nextScanStep) { diff --git a/core/view.js b/core/view.js index fdf78916..1a44d830 100644 --- a/core/view.js +++ b/core/view.js @@ -17,7 +17,7 @@ exports.View = View; const VIEW_SPECIAL_KEY_MAP_DEFAULT = { accept : [ 'return' ], exit : [ 'esc' ], - backspace : [ 'backspace', 'del' ], + backspace : [ 'backspace', 'del', 'ctrl + d'], // https://www.tecmint.com/linux-command-line-bash-shortcut-keys/ del : [ 'del' ], next : [ 'tab' ], up : [ 'up arrow' ], @@ -154,7 +154,7 @@ View.prototype.setHeight = function(height) { View.prototype.setWidth = function(width) { width = parseInt(width) || 1; - width = Math.min(width, this.client.term.termWidth); + width = Math.min(width, this.client.term.termWidth - this.position.col); this.dimens.width = width; }; @@ -186,7 +186,7 @@ View.prototype.setPropertyValue = function(propName, value) { case 'height' : this.setHeight(value); break; case 'width' : this.setWidth(value); break; - case 'focus' : this.setFocus(value); break; + case 'focus' : this.setFocusProperty(value); break; case 'text' : if('setText' in this) { @@ -252,10 +252,16 @@ View.prototype.redraw = function() { this.client.term.write(ansi.goto(this.position.row, this.position.col)); }; -View.prototype.setFocus = function(focused) { - enigAssert(this.acceptsFocus, 'View does not accept focus'); - +View.prototype.setFocusProperty = function(focused) { + // Either this should accept focus, or the focus should be false + enigAssert(this.acceptsFocus || !focused, 'View does not accept focus'); this.hasFocus = focused; +}; + +View.prototype.setFocus = function(focused) { + // Call separate method to differentiate between a value set as a + // property vs focus programmatically called. + this.setFocusProperty(focused); this.restoreCursor(); }; diff --git a/core/word_wrap.js b/core/word_wrap.js index 94773283..afeca1f8 100644 --- a/core/word_wrap.js +++ b/core/word_wrap.js @@ -1,7 +1,9 @@ /* jslint node: true */ 'use strict'; -const renderStringLength = require('./string_util.js').renderStringLength; +const { + ansiRenderStringLength, +} = require('./string_util'); // deps const assert = require('assert'); @@ -28,7 +30,7 @@ function wordWrapText(text, options) { //const REGEXP_GOBBLE = new RegExp(`.{0,${options.width}}`, 'g'); // - // For a given word, match 0->options.width chars -- alwasy include a full trailing ESC + // For a given word, match 0->options.width chars -- always include a full trailing ESC // sequence if present! // // :TODO: Need to create ansi.getMatchRegex or something - this is used all over @@ -49,7 +51,7 @@ function wordWrapText(text, options) { function appendWord() { word.match(REGEXP_GOBBLE).forEach( w => { - renderLen = renderStringLength(w); + renderLen = ansiRenderStringLength(w); if(result.renderLen[i] + renderLen > options.width) { if(0 === i) { @@ -70,7 +72,7 @@ function wordWrapText(text, options) { // // * Sublime Text 3 for example considers spaces after a word // part of said word. For example, "word " would be wraped - // in it's entirity. + // in it's entirety. // // * Tabs in Sublime Text 3 are also treated as a word, so, e.g. // "\t" may resolve to " " and must fit within the space. diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000..1e5b1711 --- /dev/null +++ b/docker/Dockerfile @@ -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"] \ No newline at end of file diff --git a/docker/bin/docker-entrypoint.sh b/docker/bin/docker-entrypoint.sh new file mode 100644 index 00000000..e96eeed9 --- /dev/null +++ b/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 diff --git a/docker/bin/sexyz b/docker/bin/sexyz new file mode 100755 index 00000000..36924822 Binary files /dev/null and b/docker/bin/sexyz differ diff --git a/docs/Gemfile b/docs/Gemfile index 1a0104dd..ec96435d 100644 --- a/docs/Gemfile +++ b/docs/Gemfile @@ -8,10 +8,10 @@ source "https://rubygems.org" # # This will help ensure the proper Jekyll version is running. # Happy Jekylling! -gem "jekyll", "~> 3.7.0" +gem "jekyll", "~> 4.2.1" # This is the default theme for new Jekyll sites. You may change this to anything you like. -gem "hacker" +# gem "hacker" # If you want to use GitHub Pages, remove the "gem "jekyll"" above and # uncomment the line below. To upgrade, run `bundle update github-pages`. @@ -19,11 +19,12 @@ gem "hacker" # If you have any plugins, put them here! group :jekyll_plugins do - gem "jekyll-feed", "~> 0.6" - gem 'jekyll-seo-tag' - gem 'jekyll-theme-hacker' - gem 'jekyll-sitemap' - gem 'jemoji' + gem 'jekyll-seo-tag', '~> 2.7.1' + gem 'jekyll-theme-hacker', '~>0.2.0' + gem 'jekyll-sitemap', '~>1.4.0' + gem 'jemoji', '~>0.12.0' + gem 'jekyll-relative-links', '~>0.6.1' + gem 'jekyll-minifier' end # Windows does not include zoneinfo files, so bundle the tzinfo-data gem diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index 55838d2b..3cdf8097 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -1,101 +1,116 @@ GEM remote: https://rubygems.org/ specs: - activesupport (4.2.9) - i18n (~> 0.7) - minitest (~> 5.1) - thread_safe (~> 0.3, >= 0.3.4) - tzinfo (~> 1.1) - addressable (2.5.2) - public_suffix (>= 2.0.2, < 4.0) + activesupport (7.0.1) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + addressable (2.8.0) + public_suffix (>= 2.0.2, < 5.0) colorator (1.1.0) - concurrent-ruby (1.0.5) - em-websocket (0.5.1) + concurrent-ruby (1.1.9) + cssminify2 (2.0.1) + em-websocket (0.5.3) eventmachine (>= 0.12.9) - http_parser.rb (~> 0.6.0) - eventmachine (1.2.5) - ffi (1.9.24) + http_parser.rb (~> 0) + eventmachine (1.2.7) + execjs (2.8.1) + ffi (1.15.5) forwardable-extended (2.6.0) - gemoji (3.0.0) - hacker (0.0.1) - html-pipeline (2.7.1) + gemoji (3.0.1) + html-pipeline (2.14.0) activesupport (>= 2) - nokogiri (>= 1.8.5) - http_parser.rb (0.6.0) - i18n (0.9.1) + nokogiri (>= 1.4) + htmlcompressor (0.4.0) + http_parser.rb (0.8.0) + i18n (1.9.1) concurrent-ruby (~> 1.0) - jekyll (3.7.4) + jekyll (4.2.1) addressable (~> 2.4) colorator (~> 1.0) em-websocket (~> 0.5) - i18n (~> 0.7) - jekyll-sass-converter (~> 1.0) + i18n (~> 1.0) + jekyll-sass-converter (~> 2.0) jekyll-watch (~> 2.0) - kramdown (~> 1.14) + kramdown (~> 2.3) + kramdown-parser-gfm (~> 1.0) liquid (~> 4.0) - mercenary (~> 0.3.3) + mercenary (~> 0.4.0) pathutil (~> 0.9) - rouge (>= 1.7, < 4) + rouge (~> 3.0) safe_yaml (~> 1.0) - jekyll-feed (0.9.2) - jekyll (~> 3.3) - jekyll-sass-converter (1.5.1) - sass (~> 3.4) - jekyll-seo-tag (2.4.0) - jekyll (~> 3.3) - jekyll-sitemap (1.1.1) - jekyll (~> 3.3) - jekyll-theme-hacker (0.1.0) - jekyll (~> 3.5) + terminal-table (~> 2.0) + jekyll-minifier (0.1.10) + cssminify2 (~> 2.0) + htmlcompressor (~> 0.4) + jekyll (>= 3.5) + json-minify (~> 0.0.3) + uglifier (~> 4.1) + jekyll-relative-links (0.6.1) + jekyll (>= 3.3, < 5.0) + jekyll-sass-converter (2.1.0) + sassc (> 2.0.1, < 3.0) + jekyll-seo-tag (2.7.1) + jekyll (>= 3.8, < 5.0) + jekyll-sitemap (1.4.0) + jekyll (>= 3.7, < 5.0) + jekyll-theme-hacker (0.2.0) + jekyll (> 3.5, < 5.0) jekyll-seo-tag (~> 2.0) - jekyll-watch (2.0.0) + jekyll-watch (2.2.1) listen (~> 3.0) - jemoji (0.8.1) - activesupport (~> 4.0, >= 4.2.9) + jemoji (0.12.0) gemoji (~> 3.0) html-pipeline (~> 2.2) - jekyll (>= 3.0) - kramdown (1.16.2) - liquid (4.0.0) - listen (3.1.5) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - ruby_dep (~> 1.2) - mercenary (0.3.6) - mini_portile2 (2.4.0) - minitest (5.11.1) - nokogiri (1.10.8) - mini_portile2 (~> 2.4.0) - pathutil (0.16.1) + jekyll (>= 3.0, < 5.0) + json (2.6.1) + json-minify (0.0.3) + json (> 0) + kramdown (2.3.1) + rexml + kramdown-parser-gfm (1.1.0) + kramdown (~> 2.0) + liquid (4.0.3) + listen (3.7.1) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + mercenary (0.4.0) + minitest (5.15.0) + nokogiri (1.13.1-x86_64-linux) + racc (~> 1.4) + pathutil (0.16.2) forwardable-extended (~> 2.6) - public_suffix (3.0.1) - rb-fsevent (0.10.2) - rb-inotify (0.9.10) - ffi (>= 1.9.24, < 2) - rouge (3.1.0) - ruby_dep (1.5.0) - safe_yaml (1.0.4) - sass (3.5.5) - sass-listen (~> 4.0.0) - sass-listen (4.0.0) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - thread_safe (0.3.6) - tzinfo (1.2.4) - thread_safe (~> 0.1) + public_suffix (4.0.6) + racc (1.6.0) + rb-fsevent (0.11.0) + rb-inotify (0.10.1) + ffi (~> 1.0) + rexml (3.2.5) + rouge (3.28.0) + safe_yaml (1.0.5) + sassc (2.4.0) + ffi (~> 1.9) + terminal-table (2.0.0) + unicode-display_width (~> 1.1, >= 1.1.1) + tzinfo (2.0.4) + concurrent-ruby (~> 1.0) + uglifier (4.2.0) + execjs (>= 0.3.0, < 3) + unicode-display_width (1.8.0) PLATFORMS - ruby + x86_64-linux DEPENDENCIES - hacker - jekyll (~> 3.7.0) - jekyll-feed (~> 0.6) - jekyll-seo-tag - jekyll-sitemap - jekyll-theme-hacker - jemoji + jekyll (~> 4.2.1) + jekyll-minifier + jekyll-relative-links (~> 0.6.1) + jekyll-seo-tag (~> 2.7.1) + jekyll-sitemap (~> 1.4.0) + jekyll-theme-hacker (~> 0.2.0) + jemoji (~> 0.12.0) tzinfo-data BUNDLED WITH - 1.16.1 + 2.3.5 diff --git a/docs/_config.yml b/docs/_config.yml index c41690bf..244f6ca4 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -9,13 +9,17 @@ logo: /assets/images/enigma-logo.png markdown: kramdown theme: jekyll-theme-hacker plugins: - - jekyll-feed - jekyll-seo-tag + - jekyll-relative-links - jekyll-sitemap - jemoji baseurl: /enigma-bbs +relative_links: + enabled: true + collections: true + # Exclude from processing. # The following items will not be processed, by default. Create a custom list # to override the default setting. @@ -28,3 +32,102 @@ exclude: - vendor/gems/ - vendor/ruby/ - .idea + + +# New documents that are not included below under order will display at the +# end of the list. Section names for directories and subdirectories are +# setup in _data/sections.yml. Change there in order to update the name of +# one of the subdirectories or to add a new one. + + +collections: + docs: + output: true + permalink: /:path:output_ext + order: + - installation/installation-methods.md + - installation/install-script.md + - installation/docker.md + - installation/manual.md + - installation/hardware/rpi.md + - installation/hardware/windows.md + - installation/network.md + - installation/testing.md + - installation/production.md + - configuration/creating-config.md + - configuration/sysop-setup.md + - configuration/config-files.md + - configuration/config-hjson.md + - configuration/hjson.md + - configuration/menu-hjson.md + - configuration/directory-structure.md + - configuration/external-binaries.md + - configuration/archivers.md + - configuration/file-transfer-protocols.md + - configuration/email.md + - configuration/colour-codes.md + - configuration/event-scheduler.md + - configuration/acs.md + - configuration/security.md + - misc/user-interrupt.md + - filebase/index.md + - filebase/first-file-area.md + - filebase/acs.md + - filebase/uploads.md + - filebase/web-access.md + - filebase/tic-support.md + - filebase/network-mounts-and-symlinks.md + - messageareas/configuring-a-message-area.md + - messageareas/message-networks.md + - messageareas/bso-import-export.md + - messageareas/netmail.md + - messageareas/qwk.md + - messageareas/ftn.md + - art/general.md + - art/themes.md + - art/mci.md + - art/views/button_view.md + - art/views/edit_text_view.md + - art/views/full_menu_view.md + - art/views/horizontal_menu_view.md + - art/views/mask_edit_text_view.md + - art/views/multi_line_edit_text_view.md + - art/views/predefined_label_view.md + - art/views/spinner_menu_view.md + - art/views/text_view.md + - art/views/toggle_menu_view.md + - art/views/vertical_menu_view.md + - servers/loginservers/telnet.md + - servers/loginservers/ssh.md + - servers/loginservers/websocket.md + - servers/contentservers/web-server.md + - servers/contentservers/gopher.md + - servers/contentservers/nntp.md + - modding/local-doors.md + - modding/door-servers.md + - modding/telnet-bridge.md + - modding/existing-mods.md + - modding/file-area-list.md + - modding/last-callers.md + - modding/whos-online.md + - modding/user-list.md + - modding/msg-conf-list.md + - modding/msg-area-list.md + - modding/bbs-list.md + - modding/rumorz.md + - modding/file-transfer-protocol-select.md + - modding/onelinerz.md + - modding/show-art.md + - modding/file-base-download-manager.md + - modding/file-base-web-download-manager.md + - modding/set-newscan-date.md + - modding/node-msg.md + - modding/top-x.md + - modding/user-2fa-otp-config.md + - modding/autosig-edit.md + - modding/menu-modules.md + - admin/administration.md + - admin/oputil.md + - admin/updating.md + - troubleshooting/monitoring-logs.md + diff --git a/docs/_data/sections.yml b/docs/_data/sections.yml new file mode 100644 index 00000000..994698e9 --- /dev/null +++ b/docs/_data/sections.yml @@ -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 diff --git a/docs/admin/administration.md b/docs/_docs/admin/administration.md similarity index 99% rename from docs/admin/administration.md rename to docs/_docs/admin/administration.md index 0b960246..b5dc7f0c 100644 --- a/docs/admin/administration.md +++ b/docs/_docs/admin/administration.md @@ -40,4 +40,4 @@ SQLite database files become less performant over time and waste space. It is re Example: ```bash sqlite3 ./db/message.sqlite3 "vacuum;" -``` \ No newline at end of file +``` diff --git a/docs/admin/oputil.md b/docs/_docs/admin/oputil.md similarity index 100% rename from docs/admin/oputil.md rename to docs/_docs/admin/oputil.md diff --git a/docs/admin/updating.md b/docs/_docs/admin/updating.md similarity index 100% rename from docs/admin/updating.md rename to docs/_docs/admin/updating.md diff --git a/docs/art/general.md b/docs/_docs/art/general.md similarity index 100% rename from docs/art/general.md rename to docs/_docs/art/general.md diff --git a/docs/art/mci.md b/docs/_docs/art/mci.md similarity index 93% rename from docs/art/mci.md rename to docs/_docs/art/mci.md index c976334a..43c08162 100644 --- a/docs/art/mci.md +++ b/docs/_docs/art/mci.md @@ -128,15 +128,17 @@ a Vertical Menu (`%VM`): Old-school BBSers may recognize this as a lightbar menu | Code | Name | Description | Notes | |------|----------------------|------------------|-------| -| `TL` | Text Label | Displays text | Static content | -| `ET` | Edit Text | Collect user input | Single line entry | -| `ME` | Masked Edit Text | Collect user input using a *mask* | See **Mask Edits** below | -| `MT` | Multi Line Text Edit | Multi line edit control | Used for FSE, display of FILE_ID.DIZ, etc. | -| `BT` | Button | A button | ...it's a button | -| `VM` | Vertical Menu | A vertical menu | AKA a vertical lightbar; Useful for lists | -| `HM` | Horizontal Menu | A horizontal menu | AKA a horizontal lightbar | -| `SM` | Spinner Menu | A spinner input control | Select *one* from multiple options | -| `TM` | Toggle Menu | A toggle menu | Commonly used for Yes/No style input | +| `TL` | Text Label | Displays text | Static content. See [Text View](views/text_view.md) | +| `ET` | Edit Text | Collect user input | Single line entry. See [Edit Text](views/edit_text_view.md) | +| `ME` | Masked Edit Text | Collect user input using a *mask* | See [Masked Edit](views/mask_edit_text_view.md) and **Mask Edits** below. | +| `MT` | Multi Line Text Edit | Multi line edit control | Used for FSE, display of FILE_ID.DIZ, etc. See [Multiline Text Edit](views/multi_line_edit_text_view.md) | +| `BT` | Button | A button | ...it's a button. See [Button](views/button_view.md) | +| `VM` | Vertical Menu | A vertical menu | AKA a vertical lightbar; Useful for lists. See [Vertical Menu](views/vertical_menu_view.md) | +| `HM` | Horizontal Menu | A horizontal menu | AKA a horizontal lightbar. See [Horizontal Menu](views/horizontal_menu_view.md) | +| `FM` | Full Menu | A menu that can go both vertical and horizontal. | See [Full Menu](views/full_menu_view.md) | +| `SM` | Spinner Menu | A spinner input control | Select *one* from multiple options. See [Spinner Menu](views/spinner_menu_view.md) | +| `TM` | Toggle Menu | A toggle menu | Commonly used for Yes/No style input. See [Toggle Menu](views/toggle_menu_view.md)| +| `PL` | Predefined Label | Show environment information | See [Predefined Label](views/predefined_label_view.md)| | `KE` | Key Entry | A *single* key input control | Think hotkeys | :information_source: Peek at [/core/mci_view_factory.js](https://github.com/NuSkooler/enigma-bbs/blob/master/core/mci_view_factory.js) to see additional information. @@ -245,4 +247,4 @@ Suppose a format object contains the following elements: `userName` and `affils` ![Example](../assets/images/text-format-example1.png "Text Format") -:bulb: Remember that a Python [string format mini language](https://docs.python.org/3/library/string.html#format-specification-mini-language) style syntax is available for widths, alignment, number prevision, etc. as well. A number can be made to be more human readable for example: `{byteSize:,}` may yield "1,123,456". \ No newline at end of file +:bulb: Remember that a Python [string format mini language](https://docs.python.org/3/library/string.html#format-specification-mini-language) style syntax is available for widths, alignment, number prevision, etc. as well. A number can be made to be more human readable for example: `{byteSize:,}` may yield "1,123,456". diff --git a/docs/art/themes.md b/docs/_docs/art/themes.md similarity index 100% rename from docs/art/themes.md rename to docs/_docs/art/themes.md diff --git a/docs/_docs/art/views/button_view.md b/docs/_docs/art/views/button_view.md new file mode 100644 index 00000000..65a753ca --- /dev/null +++ b/docs/_docs/art/views/button_view.md @@ -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") + +
+Configuration fragment (expand to view) +
+``` +BT1: { + submit: true + justify: center + argName: btnSelect + width: 17 + focusTextStyle: upper + text: Centered button +} +``` +
+
diff --git a/docs/_docs/art/views/edit_text_view.md b/docs/_docs/art/views/edit_text_view.md new file mode 100644 index 00000000..c372246d --- /dev/null +++ b/docs/_docs/art/views/edit_text_view.md @@ -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") + +
+Configuration fragment (expand to view) +
+``` +ET1: { + maxLength: @config:users.usernameMax + argName: username + focus: true +} +``` +
+
diff --git a/docs/_docs/art/views/full_menu_view.md b/docs/_docs/art/views/full_menu_view.md new file mode 100644 index 00000000..19ff365a --- /dev/null +++ b/docs/_docs/art/views/full_menu_view.md @@ -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") + +
+Configuration fragment (expand to view) +
+``` +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 + } + ] +} + +``` +
+
+ +### A simple horizontal menu - similar to HM + +![Example](../../assets/images/full_menu_view_example2.gif "Horizontal menu") + +
+Configuration fragment (expand to view) +
+``` +FM2: { + focus: true + height: 1 + width: 60 // set as desired + submit: true + argName: navSelect + items: [ + "prev", "next", "details", "toggle queue", "rate", "help", "quit" + ] +} +``` +
+
+ +### A multi-column navigation menu with hotkeys + + +![Example](../../assets/images/full_menu_view_example3.gif "Multi column menu") + +
+Configuration fragment (expand to view) +
+``` +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 + } + ] +} +``` +
+
+ diff --git a/docs/_docs/art/views/horizontal_menu_view.md b/docs/_docs/art/views/horizontal_menu_view.md new file mode 100644 index 00000000..90dc4438 --- /dev/null +++ b/docs/_docs/art/views/horizontal_menu_view.md @@ -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") + +
+Configuration fragment (expand to view) +
+``` +HM2: { + focus: true + width: 60 // set as desired + submit: true + argName: navSelect + items: [ + "prev", "next", "details", "toggle queue", "rate", "help", "quit" + ] +} +``` +
+
diff --git a/docs/_docs/art/views/mask_edit_text_view.md b/docs/_docs/art/views/mask_edit_text_view.md new file mode 100644 index 00000000..a03e83c5 --- /dev/null +++ b/docs/_docs/art/views/mask_edit_text_view.md @@ -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") + +
+Configuration fragment (expand to view) +
+``` +ME1: { + argName: height + fillChar: "#" + maskPattern: "# ft. ## in." +} +``` +
+
diff --git a/docs/_docs/art/views/multi_line_edit_text_view.md b/docs/_docs/art/views/multi_line_edit_text_view.md new file mode 100644 index 00000000..870360ba --- /dev/null +++ b/docs/_docs/art/views/multi_line_edit_text_view.md @@ -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") + +
+Configuration fragment (expand to view) +
+``` +ML1: { + width: 79 + argName: message + mode: edit +} +``` +
+
diff --git a/docs/_docs/art/views/predefined_label_view.md b/docs/_docs/art/views/predefined_label_view.md new file mode 100644 index 00000000..cae23f55 --- /dev/null +++ b/docs/_docs/art/views/predefined_label_view.md @@ -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") + +
+Configuration fragment (expand to view) +
+``` +PL1: { + textStyle: upper +} +``` +
+
diff --git a/docs/_docs/art/views/spinner_menu_view.md b/docs/_docs/art/views/spinner_menu_view.md new file mode 100644 index 00000000..0f7139f8 --- /dev/null +++ b/docs/_docs/art/views/spinner_menu_view.md @@ -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") + +
+Configuration fragment (expand to view) +
+``` +SM1: { + submit: true + argName: themeSelect + items: [ + { + text: Light + data: light + } + { + text: Dark + data: dark + } + { + text: Rainbow + data: rainbow + } + { + text: Gruvbox + data: gruvbox + } + ] +} + +``` +
+
diff --git a/docs/_docs/art/views/text_view.md b/docs/_docs/art/views/text_view.md new file mode 100644 index 00000000..3bec8ed8 --- /dev/null +++ b/docs/_docs/art/views/text_view.md @@ -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") + +
+Configuration fragment (expand to view) +
+``` +TL1: { + text: Text label +} +``` +
+
diff --git a/docs/_docs/art/views/toggle_menu_view.md b/docs/_docs/art/views/toggle_menu_view.md new file mode 100644 index 00000000..65c1eabd --- /dev/null +++ b/docs/_docs/art/views/toggle_menu_view.md @@ -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") + +
+Configuration fragment (expand to view) +
+``` +TM2: { + focus: true + submit: true + argName: navSelect + focusTextStyle: upper + items: [ "yes", "no" ] +} +``` +
+
diff --git a/docs/_docs/art/views/vertical_menu_view.md b/docs/_docs/art/views/vertical_menu_view.md new file mode 100644 index 00000000..e46f92ae --- /dev/null +++ b/docs/_docs/art/views/vertical_menu_view.md @@ -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") + +
+Configuration fragment (expand to view) +
+``` +VM1: { + submit: true + argName: navSelect + items: [ + { + text: login + data: login + } + { + text: apply + data: new user + } + { + text: about + data: about + } + { + text: log off + data: logoff + } + ] +} + +``` +
+
diff --git a/docs/configuration/acs.md b/docs/_docs/configuration/acs.md similarity index 100% rename from docs/configuration/acs.md rename to docs/_docs/configuration/acs.md diff --git a/docs/configuration/archivers.md b/docs/_docs/configuration/archivers.md similarity index 100% rename from docs/configuration/archivers.md rename to docs/_docs/configuration/archivers.md diff --git a/docs/configuration/colour-codes.md b/docs/_docs/configuration/colour-codes.md similarity index 100% rename from docs/configuration/colour-codes.md rename to docs/_docs/configuration/colour-codes.md diff --git a/docs/configuration/config-files.md b/docs/_docs/configuration/config-files.md similarity index 100% rename from docs/configuration/config-files.md rename to docs/_docs/configuration/config-files.md diff --git a/docs/configuration/config-hjson.md b/docs/_docs/configuration/config-hjson.md similarity index 100% rename from docs/configuration/config-hjson.md rename to docs/_docs/configuration/config-hjson.md diff --git a/docs/configuration/creating-config.md b/docs/_docs/configuration/creating-config.md similarity index 100% rename from docs/configuration/creating-config.md rename to docs/_docs/configuration/creating-config.md diff --git a/docs/configuration/directory-structure.md b/docs/_docs/configuration/directory-structure.md similarity index 69% rename from docs/configuration/directory-structure.md rename to docs/_docs/configuration/directory-structure.md index 4060991c..8f318ac4 100644 --- a/docs/configuration/directory-structure.md +++ b/docs/_docs/configuration/directory-structure.md @@ -6,17 +6,17 @@ All paths mentioned here are relative to the ENiGMA½ checkout directory. | Directory | Description | |---------------------|-----------------------------------------------------------------------------------------------------------| -| `/art/general` | Non-theme art - welcome ANSI, logoff ANSI, etc. See [General Art]({{ site.baseurl }}{% link art/general.md %}). -| `/art/themes` | Theme art. Themes should be in their own subdirectory and contain a theme.hjson. See [Themes]({{ site.baseurl }}{% link art/themes.md %}). +| `/art/general` | Non-theme art - welcome ANSI, logoff ANSI, etc. See [General Art](../art/general.md). +| `/art/themes` | Theme art. Themes should be in their own subdirectory and contain a theme.hjson. See [Themes](../art/themes.md). | `/config` | [config.hjson](config-hjson.md) system configuration. | `/config/menus` | [menu.hjson](menu-hjson.md) storage. | `/config/security` | SSL certificates and public/private keys. | `/db` | All ENiGMA½ databases in SQLite3 format. | `/docs` | These docs ;-) -| `/dropfiles` | Dropfiles created for [local doors]({{ site.baseurl }}{% link modding/local-doors.md %}) -| `/logs` | Logs. See [Monitoring Logs]({{ site.baseurl }}{% link troubleshooting/monitoring-logs.md %}) +| `/dropfiles` | Dropfiles created for [local doors](../modding/local-doors.md) +| `/logs` | Logs. See [Monitoring Logs](../troubleshooting/monitoring-logs.md) | `/misc` | Stuff with no other home; reset password templates, common password lists, other random bits -| `/mods` | User mods. See [Modding]({{ site.baseurl }}{% link modding/existing-mods.md %}) +| `/mods` | User mods. See [Modding](../modding/existing-mods.md) | `/node_modules` | External libraries required by ENiGMA½, installed when you run `npm install` | `/util` | Various tools used in running/debugging ENiGMA½ -| `/www` | ENiGMA½'s built in webserver root directory \ No newline at end of file +| `/www` | ENiGMA½'s built in webserver root directory diff --git a/docs/configuration/email.md b/docs/_docs/configuration/email.md similarity index 86% rename from docs/configuration/email.md rename to docs/_docs/configuration/email.md index eb13ef71..b8418181 100644 --- a/docs/configuration/email.md +++ b/docs/_docs/configuration/email.md @@ -3,7 +3,7 @@ layout: page title: Email --- ## Email Support -ENiGMA½ uses email to send password reset information to users. For it to work, you need to provide valid [Nodemailer](https://nodemailer.com/about/) compatible `email` block in your [config.hjson]({{ site.baseurl }}{% link configuration/config-hjson.md %}). Nodemailer supports SMTP in addition to many pre-defined services for ease of use. The `transport` block within `email` must be Nodemailer compatible. +ENiGMA½ uses email to send password reset information to users. For it to work, you need to provide valid [Nodemailer](https://nodemailer.com/about/) compatible `email` block in your [config.hjson](config-hjson.md). Nodemailer supports SMTP in addition to many pre-defined services for ease of use. The `transport` block within `email` must be Nodemailer compatible. Additional email support will come in the near future. diff --git a/docs/configuration/event-scheduler.md b/docs/_docs/configuration/event-scheduler.md similarity index 100% rename from docs/configuration/event-scheduler.md rename to docs/_docs/configuration/event-scheduler.md diff --git a/docs/configuration/external-binaries.md b/docs/_docs/configuration/external-binaries.md similarity index 100% rename from docs/configuration/external-binaries.md rename to docs/_docs/configuration/external-binaries.md diff --git a/docs/configuration/file-transfer-protocols.md b/docs/_docs/configuration/file-transfer-protocols.md similarity index 100% rename from docs/configuration/file-transfer-protocols.md rename to docs/_docs/configuration/file-transfer-protocols.md diff --git a/docs/configuration/hjson.md b/docs/_docs/configuration/hjson.md similarity index 100% rename from docs/configuration/hjson.md rename to docs/_docs/configuration/hjson.md diff --git a/docs/configuration/menu-hjson.md b/docs/_docs/configuration/menu-hjson.md similarity index 100% rename from docs/configuration/menu-hjson.md rename to docs/_docs/configuration/menu-hjson.md diff --git a/docs/configuration/security.md b/docs/_docs/configuration/security.md similarity index 100% rename from docs/configuration/security.md rename to docs/_docs/configuration/security.md diff --git a/docs/configuration/sysop-setup.md b/docs/_docs/configuration/sysop-setup.md similarity index 100% rename from docs/configuration/sysop-setup.md rename to docs/_docs/configuration/sysop-setup.md diff --git a/docs/filebase/acs.md b/docs/_docs/filebase/acs.md similarity index 100% rename from docs/filebase/acs.md rename to docs/_docs/filebase/acs.md diff --git a/docs/filebase/first-file-area.md b/docs/_docs/filebase/first-file-area.md similarity index 96% rename from docs/filebase/first-file-area.md rename to docs/_docs/filebase/first-file-area.md index ae9e0fd2..db1efed8 100644 --- a/docs/filebase/first-file-area.md +++ b/docs/_docs/filebase/first-file-area.md @@ -36,6 +36,7 @@ File base *Areas* are configured using the `fileBase.areas` configuration block | `desc` | :-1: | Friendly area description. | | `storageTags` | :+1: | An array of storage tags for physical storage backing of the files in this area. If uploads are enabled for this area, **first** storage tag location is utilized! | | `sort` | :-1: | If present, provides the sort key for ordering. `name` is used otherwise. | +| `hashTags` | :-1: | Set to an array of strings or comma separated list to provide _default_ hash tags for this area. | Example areas section: @@ -45,6 +46,7 @@ areas: { name: Retro PC desc: Oldschool PC/DOS storageTags: [ "retro_pc_dos", "retro_pc_bbs" ] + hashTags: ["retro", "pc", "dos" ] } } ``` diff --git a/docs/filebase/index.md b/docs/_docs/filebase/index.md similarity index 100% rename from docs/filebase/index.md rename to docs/_docs/filebase/index.md diff --git a/docs/filebase/network-mounts-and-symlinks.md b/docs/_docs/filebase/network-mounts-and-symlinks.md similarity index 100% rename from docs/filebase/network-mounts-and-symlinks.md rename to docs/_docs/filebase/network-mounts-and-symlinks.md diff --git a/docs/filebase/tic-support.md b/docs/_docs/filebase/tic-support.md similarity index 100% rename from docs/filebase/tic-support.md rename to docs/_docs/filebase/tic-support.md diff --git a/docs/filebase/uploads.md b/docs/_docs/filebase/uploads.md similarity index 100% rename from docs/filebase/uploads.md rename to docs/_docs/filebase/uploads.md diff --git a/docs/filebase/web-access.md b/docs/_docs/filebase/web-access.md similarity index 100% rename from docs/filebase/web-access.md rename to docs/_docs/filebase/web-access.md diff --git a/docs/_docs/installation/docker.md b/docs/_docs/installation/docker.md new file mode 100644 index 00000000..1fa1de49 --- /dev/null +++ b/docs/_docs/installation/docker.md @@ -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 . + ``` diff --git a/docs/installation/rpi.md b/docs/_docs/installation/hardware/rpi.md similarity index 100% rename from docs/installation/rpi.md rename to docs/_docs/installation/hardware/rpi.md diff --git a/docs/installation/windows.md b/docs/_docs/installation/hardware/windows.md similarity index 100% rename from docs/installation/windows.md rename to docs/_docs/installation/hardware/windows.md diff --git a/docs/installation/install-script.md b/docs/_docs/installation/install-script.md similarity index 100% rename from docs/installation/install-script.md rename to docs/_docs/installation/install-script.md diff --git a/docs/installation/installation-methods.md b/docs/_docs/installation/installation-methods.md similarity index 100% rename from docs/installation/installation-methods.md rename to docs/_docs/installation/installation-methods.md diff --git a/docs/installation/manual.md b/docs/_docs/installation/manual.md similarity index 93% rename from docs/installation/manual.md rename to docs/_docs/installation/manual.md index cb463d81..eb8101aa 100644 --- a/docs/installation/manual.md +++ b/docs/_docs/installation/manual.md @@ -6,7 +6,7 @@ For Linux environments it's recommended you run the [install script](install-scr do things manually, read on... ## Prerequisites -* [Node.js](https://nodejs.org/) version **v12.x LTS or higher** (Other versions may work but are not supported). +* [Node.js](https://nodejs.org/) version **v14.x LTS or higher**. Versions under v14 are known not to work due to language level changes. * :bulb: It is **highly** recommended to use [Node Version Manager (NVM)](https://github.com/creationix/nvm) to manage your Node.js installation if you're on a Linux/Unix environment. * [Python](https://www.python.org/downloads/) for compiling Node.js packages with native extensions via `node-gyp`. @@ -57,7 +57,7 @@ ENiGMA BBS makes use of a few packages for archive and legacy protocol support. :information_source: Additional information in [Archivers](../configuration/archivers.md) and [File Transfer Protocols](../configuration/file-transfer-protocols.md) ## Config Files -You'll need a basic configuration to get started. The main system configuration is handled via `config/config.hjson`. This is an [HJSON](http://hjson.org/) file (compliant JSON is also OK). See [Configuration](../configuration/) for more information. +You'll need a basic configuration to get started. The main system configuration is handled via `config/config.hjson`. This is an [HJSON](http://hjson.org/) file (compliant JSON is also OK). See [Configuration](../configuration/hjson.md) for more information. Use `oputil.js` to generate your **initial** configuration: diff --git a/docs/installation/network.md b/docs/_docs/installation/network.md similarity index 100% rename from docs/installation/network.md rename to docs/_docs/installation/network.md diff --git a/docs/installation/production.md b/docs/_docs/installation/production.md similarity index 100% rename from docs/installation/production.md rename to docs/_docs/installation/production.md diff --git a/docs/installation/testing.md b/docs/_docs/installation/testing.md similarity index 97% rename from docs/installation/testing.md rename to docs/_docs/installation/testing.md index 2e5dbea1..1efc47cc 100644 --- a/docs/installation/testing.md +++ b/docs/_docs/installation/testing.md @@ -13,7 +13,7 @@ _Note that if you've used the [Docker](docker.md) installation method, you've al If everything went OK: ```bash -ENiGMA½ Copyright (c) 2014-2020, Bryan Ashby +ENiGMA½ Copyright (c) 2014-2022, Bryan Ashby _____________________ _____ ____________________ __________\_ / \__ ____/\_ ____ \ /____/ / _____ __ \ / ______/ // /___jp! // __|___// | \// |// | \// | | \// \ /___ /_____ diff --git a/docs/messageareas/bso-import-export.md b/docs/_docs/messageareas/bso-import-export.md similarity index 100% rename from docs/messageareas/bso-import-export.md rename to docs/_docs/messageareas/bso-import-export.md diff --git a/docs/messageareas/configuring-a-message-area.md b/docs/_docs/messageareas/configuring-a-message-area.md similarity index 100% rename from docs/messageareas/configuring-a-message-area.md rename to docs/_docs/messageareas/configuring-a-message-area.md diff --git a/docs/messageareas/ftn.md b/docs/_docs/messageareas/ftn.md similarity index 100% rename from docs/messageareas/ftn.md rename to docs/_docs/messageareas/ftn.md diff --git a/docs/messageareas/message-networks.md b/docs/_docs/messageareas/message-networks.md similarity index 100% rename from docs/messageareas/message-networks.md rename to docs/_docs/messageareas/message-networks.md diff --git a/docs/messageareas/netmail.md b/docs/_docs/messageareas/netmail.md similarity index 100% rename from docs/messageareas/netmail.md rename to docs/_docs/messageareas/netmail.md diff --git a/docs/messageareas/qwk.md b/docs/_docs/messageareas/qwk.md similarity index 100% rename from docs/messageareas/qwk.md rename to docs/_docs/messageareas/qwk.md diff --git a/docs/misc/user-interrupt.md b/docs/_docs/misc/user-interrupt.md similarity index 100% rename from docs/misc/user-interrupt.md rename to docs/_docs/misc/user-interrupt.md diff --git a/docs/modding/autosig-edit.md b/docs/_docs/modding/autosig-edit.md similarity index 100% rename from docs/modding/autosig-edit.md rename to docs/_docs/modding/autosig-edit.md diff --git a/docs/modding/bbs-list.md b/docs/_docs/modding/bbs-list.md similarity index 100% rename from docs/modding/bbs-list.md rename to docs/_docs/modding/bbs-list.md diff --git a/docs/modding/door-servers.md b/docs/_docs/modding/door-servers.md similarity index 100% rename from docs/modding/door-servers.md rename to docs/_docs/modding/door-servers.md diff --git a/docs/modding/existing-mods.md b/docs/_docs/modding/existing-mods.md similarity index 100% rename from docs/modding/existing-mods.md rename to docs/_docs/modding/existing-mods.md diff --git a/docs/modding/file-area-list.md b/docs/_docs/modding/file-area-list.md similarity index 100% rename from docs/modding/file-area-list.md rename to docs/_docs/modding/file-area-list.md diff --git a/docs/modding/file-base-download-manager.md b/docs/_docs/modding/file-base-download-manager.md similarity index 100% rename from docs/modding/file-base-download-manager.md rename to docs/_docs/modding/file-base-download-manager.md diff --git a/docs/modding/file-base-web-download-manager.md b/docs/_docs/modding/file-base-web-download-manager.md similarity index 100% rename from docs/modding/file-base-web-download-manager.md rename to docs/_docs/modding/file-base-web-download-manager.md diff --git a/docs/modding/file-transfer-protocol-select.md b/docs/_docs/modding/file-transfer-protocol-select.md similarity index 100% rename from docs/modding/file-transfer-protocol-select.md rename to docs/_docs/modding/file-transfer-protocol-select.md diff --git a/docs/modding/last-callers.md b/docs/_docs/modding/last-callers.md similarity index 100% rename from docs/modding/last-callers.md rename to docs/_docs/modding/last-callers.md diff --git a/docs/modding/local-doors.md b/docs/_docs/modding/local-doors.md similarity index 95% rename from docs/modding/local-doors.md rename to docs/_docs/modding/local-doors.md index 3e3e59ad..ce948875 100644 --- a/docs/modding/local-doors.md +++ b/docs/_docs/modding/local-doors.md @@ -5,6 +5,8 @@ title: Local Doors ## Local Doors ENiGMA½ has many ways to add doors to your system. In addition to the [many built in door server modules](door-servers.md), local doors are of course also supported using the ! The `abracadabra` module! +:information_source: See also [Let’s add a DOS door to Enigma½ BBS](https://medium.com/retro-future/lets-add-a-dos-game-to-enigma-1-2-41f257deaa3c) by Robbie Whiting for a great writeup on adding doors! + ## The abracadabra Module The `abracadabra` module provides a generic and flexible solution for many door types. Through this module you can execute native processes & scripts directly, and perform I/O through standard I/O (stdio) or a temporary TCP server. @@ -14,10 +16,11 @@ The `abracadabra` `config` block can contain the following members: | Item | Required | Description | |------|----------|-------------| | `name` | :+1: | Used as a key for tracking number of clients using a particular door. | -| `dropFileType` | :+1: | Specifies the type of dropfile to generate (See **Dropfile Types** below). | +| `dropFileType` | :-1: | Specifies the type of dropfile to generate (See **Dropfile Types** below). Can be omitted or set to `none`. | | `cmd` | :+1: | Path to executable to launch. | | `args` | :-1: | Array of argument(s) to pass to `cmd`. See **Argument Variables** below for information on variables that can be used here. | `cwd` | :-1: | Sets the Current Working Directory (CWD) for `cmd`. Defaults to the directory of `cmd`. | +| `env` | :-1: | Sets the environment. Supplied in the form of an map: `{ SOME_VAR: "value" }` | `nodeMax` | :-1: | Max number of nodes that can access this door at once. Uses `name` as a tracking key. | | `tooManyArt` | :-1: | Art spec to display if too many instances are already in use. | | `io` | :-1: | How to process input/output (I/O). Can be `stdio` or `socket`. When using `stdio`, I/O is handled via standard stdin/stdout. When using `socket` a temporary socket server is spawned that can be connected back to. The server listens on localhost on `{srvPort}` (See **Argument Variables** below for more information). Default value is `stdio`. | diff --git a/docs/modding/menu-modules.md b/docs/_docs/modding/menu-modules.md similarity index 88% rename from docs/modding/menu-modules.md rename to docs/_docs/modding/menu-modules.md index 1a2a9133..0ab7baea 100644 --- a/docs/modding/menu-modules.md +++ b/docs/_docs/modding/menu-modules.md @@ -1,6 +1,6 @@ --- layout: page -title: Local Doors +title: Menu Modules --- ## Menu Modules Menu entries found within `menu.hjson` are backed by *menu modules*. diff --git a/docs/modding/msg-area-list.md b/docs/_docs/modding/msg-area-list.md similarity index 100% rename from docs/modding/msg-area-list.md rename to docs/_docs/modding/msg-area-list.md diff --git a/docs/modding/msg-conf-list.md b/docs/_docs/modding/msg-conf-list.md similarity index 100% rename from docs/modding/msg-conf-list.md rename to docs/_docs/modding/msg-conf-list.md diff --git a/docs/modding/node-msg.md b/docs/_docs/modding/node-msg.md similarity index 100% rename from docs/modding/node-msg.md rename to docs/_docs/modding/node-msg.md diff --git a/docs/modding/onelinerz.md b/docs/_docs/modding/onelinerz.md similarity index 100% rename from docs/modding/onelinerz.md rename to docs/_docs/modding/onelinerz.md diff --git a/docs/modding/rumorz.md b/docs/_docs/modding/rumorz.md similarity index 100% rename from docs/modding/rumorz.md rename to docs/_docs/modding/rumorz.md diff --git a/docs/modding/set-newscan-date.md b/docs/_docs/modding/set-newscan-date.md similarity index 100% rename from docs/modding/set-newscan-date.md rename to docs/_docs/modding/set-newscan-date.md diff --git a/docs/modding/show-art.md b/docs/_docs/modding/show-art.md similarity index 100% rename from docs/modding/show-art.md rename to docs/_docs/modding/show-art.md diff --git a/docs/modding/telnet-bridge.md b/docs/_docs/modding/telnet-bridge.md similarity index 100% rename from docs/modding/telnet-bridge.md rename to docs/_docs/modding/telnet-bridge.md diff --git a/docs/modding/top-x.md b/docs/_docs/modding/top-x.md similarity index 100% rename from docs/modding/top-x.md rename to docs/_docs/modding/top-x.md diff --git a/docs/modding/user-2fa-otp-config.md b/docs/_docs/modding/user-2fa-otp-config.md similarity index 99% rename from docs/modding/user-2fa-otp-config.md rename to docs/_docs/modding/user-2fa-otp-config.md index 4ef12687..f2e5f945 100644 --- a/docs/modding/user-2fa-otp-config.md +++ b/docs/_docs/modding/user-2fa-otp-config.md @@ -1,6 +1,6 @@ --- layout: page -title: TopX +title: 2FA/OTP Config --- ## The 2FA/OTP Config Module The `user_2fa_otp_config` module provides opt-in, configuration, and viewing of Two-Factor Authentication via One-Time-Password (2FA/OTP) settings. In order to allow users access to 2FA/OTP, the system must be properly configured. See [Security](../configuration/security.md) for more information. diff --git a/docs/modding/user-list.md b/docs/_docs/modding/user-list.md similarity index 100% rename from docs/modding/user-list.md rename to docs/_docs/modding/user-list.md diff --git a/docs/modding/whos-online.md b/docs/_docs/modding/whos-online.md similarity index 100% rename from docs/modding/whos-online.md rename to docs/_docs/modding/whos-online.md diff --git a/docs/servers/gopher.md b/docs/_docs/servers/contentservers/gopher.md similarity index 100% rename from docs/servers/gopher.md rename to docs/_docs/servers/contentservers/gopher.md diff --git a/docs/servers/nntp.md b/docs/_docs/servers/contentservers/nntp.md similarity index 100% rename from docs/servers/nntp.md rename to docs/_docs/servers/contentservers/nntp.md diff --git a/docs/servers/web-server.md b/docs/_docs/servers/contentservers/web-server.md similarity index 100% rename from docs/servers/web-server.md rename to docs/_docs/servers/contentservers/web-server.md diff --git a/docs/servers/ssh.md b/docs/_docs/servers/loginservers/ssh.md similarity index 100% rename from docs/servers/ssh.md rename to docs/_docs/servers/loginservers/ssh.md diff --git a/docs/servers/telnet.md b/docs/_docs/servers/loginservers/telnet.md similarity index 100% rename from docs/servers/telnet.md rename to docs/_docs/servers/loginservers/telnet.md diff --git a/docs/servers/websocket.md b/docs/_docs/servers/loginservers/websocket.md similarity index 94% rename from docs/servers/websocket.md rename to docs/_docs/servers/loginservers/websocket.md index 1bfa0583..a2c26754 100644 --- a/docs/servers/websocket.md +++ b/docs/_docs/servers/loginservers/websocket.md @@ -6,7 +6,7 @@ title: Web Socket / Web Interface Server The WebSocket Login Server provides **secure** (wss://) as well as non-secure (ws://) WebSocket login access. This is often combined with a browser based WebSocket client such as VTX or fTelnet. # VTX Web Client -ENiGMA supports the VTX websocket client for connecting to your BBS from a web page. Example usage can be found at [Xibalba](https://xibalba.l33t.codes) and [fORCE9](https://bbs.force9.org/vtx/force9.html) amongst others. +ENiGMA supports the VTX WebSocket client for connecting to your BBS from a web page. Example usage can be found at [Xibalba](https://xibalba.l33t.codes) and [fORCE9](https://bbs.force9.org/vtx/force9.html) amongst others. ## Before You Start There are a few things out of scope of this document: @@ -62,7 +62,7 @@ following: 3. Download the [VTX_ClientServer](https://github.com/codewar65/VTX_ClientServer/archive/master.zip) to your webserver, and unpack it to a temporary directory. -4. Download the example [VTX client HTML file](/misc/vtx/vtx.html) and save it to your webserver root. +4. Download the example [VTX client HTML file](https://raw.githubusercontent.com/NuSkooler/enigma-bbs/master/misc/vtx/vtx.html) and save it to your webserver root. 5. Create an `assets/vtx` directory within your webserver root, so you have a structure like the following: diff --git a/docs/troubleshooting/monitoring-logs.md b/docs/_docs/troubleshooting/monitoring-logs.md similarity index 100% rename from docs/troubleshooting/monitoring-logs.md rename to docs/_docs/troubleshooting/monitoring-logs.md diff --git a/docs/_includes/nav.html b/docs/_includes/nav.html new file mode 100644 index 00000000..e3272d9c --- /dev/null +++ b/docs/_includes/nav.html @@ -0,0 +1,70 @@ +
    +{% for doc in site.docs %} + {% assign pathparts = doc.path | split: '/' %} + {% assign dir = pathparts[1] %} + + {% if pathparts.size > 3 %} + {% assign subdir = pathparts[2] %} + {% unless site.data.sections[subdir] %} + {% assign subsection = subdir %} + {% else %} + {% assign subsection = site.data.sections[subdir].title %} + {% endunless %} + {% else %} + {% assign subdir = "NONE" %} + {% endif %} + + {% assign section = site.data.sections[dir].title %} + {% unless section %} + {% assign section = dir %} + {% endunless %} + + {% if doc.previous %} + {% assign prevpathparts = doc.previous.path | split: '/' %} + {% assign prevdir = prevpathparts[1] %} + + {% if prevpathparts.size > 3 %} + {% assign prevsubdir = prevpathparts[2] %} + {% else %} + {% assign prevsubdir = "NONE" %} + {% endif %} + {% else %} + {% assign prevdir = "NONE" %} + {% assign prevsubdir = "NONE" %} + {% endif %} + + {% if subdir != prevsubdir and prevsubdir != "NONE" %} +
+ {% endif %} + + {% if dir != prevdir %} + {% if prevdir != "NONE" %} + + {% endif %} +
  • {{section}}
  • +
      + + {% endif %} + + {% if subdir != "NONE" and subdir != prevsubdir %} +
    • {{subsection}}
    • +
        + {% endif %} + + + {% if doc.url != page.url %} +
      • {{doc.title}}
      • + {% else %} +
      • {{doc.title}}
      • + {% endif %} + + + + {% unless doc.next %} +
      + {% if prevsubdir != "NONE" %} +
    + {% endif %} + {% endunless %} +{% endfor %} + diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md deleted file mode 100644 index 42cc7bff..00000000 --- a/docs/_includes/nav.md +++ /dev/null @@ -1,98 +0,0 @@ - - Installation - - [Installation Methods]({{ site.baseurl }}{% link installation/installation-methods.md %}) - - [Install script]({{ site.baseurl }}{% link installation/install-script.md %}) - - [Docker]({{ site.baseurl }}{% link installation/docker.md %}) - - [Manual installation]({{ site.baseurl }}{% link installation/manual.md %}) - - [OS / Hardware Specific]({{ site.baseurl }}{% link installation/os-hardware.md %}) - - [Raspberry Pi]({{ site.baseurl }}{% link installation/rpi.md %}) - - [Windows]({{ site.baseurl }}{% link installation/windows.md %}) - - [Your Network Setup]({{ site.baseurl }}{% link installation/network.md %}) - - [Testing Your Installation]({{ site.baseurl }}{% link installation/testing.md %}) - - [Production Installation]({{ site.baseurl }}{% link installation/production.md %}) - - - Configuration - - [Creating Config Files]({{ site.baseurl }}{% link configuration/creating-config.md %}) - - [SysOp Setup]({{ site.baseurl }}{% link configuration/sysop-setup.md %}) - - [Configuration Files]({{ site.baseurl }}{% link configuration/config-files.md %}) - - [System Configuration]({{ site.baseurl }}{% link configuration/config-hjson.md %}) - - [HJSON Config Files]({{ site.baseurl }}{% link configuration/hjson.md %}) - - [Menus]({{ site.baseurl }}{% link configuration/menu-hjson.md %}) - - [Directory Structure]({{ site.baseurl }}{% link configuration/directory-structure.md %}) - - [External Binaries]({{ site.baseurl }}{% link configuration/external-binaries.md %}) - - [Archivers]({{ site.baseurl }}{% link configuration/archivers.md %}) - - [File Transfer Protocols]({{ site.baseurl }}{% link configuration/file-transfer-protocols.md %}) - - [Email]({{ site.baseurl }}{% link configuration/email.md %}) - - [Colour Codes]({{ site.baseurl }}{% link configuration/colour-codes.md %}) - - [Access Condition System (ACS)]({{ site.baseurl }}{% link configuration/acs.md %}) - - [Event Scheduler]({{ site.baseurl }}{% link configuration/event-scheduler.md %}) - - [Security]({{ site.baseurl }}{% link configuration/security.md %}) - - - File Base - - [About]({{ site.baseurl }}{% link filebase/index.md %}) - - [Configuring a File Area]({{ site.baseurl }}{% link filebase/first-file-area.md %}) - - [ACS model]({{ site.baseurl }}{% link filebase/acs.md %}) - - [Uploads]({{ site.baseurl }}{% link filebase/uploads.md %}) - - [Web Access]({{ site.baseurl }}{% link filebase/web-access.md %}) - - [TIC Support]({{ site.baseurl }}{% link filebase/tic-support.md %}) (Importing from FTN networks) - - Tips and tricks - - [Network mounts and symlinks]({{ site.baseurl }}{% link filebase/network-mounts-and-symlinks.md %}) - - - Message Areas - - [Configuring a Message Area]({{ site.baseurl }}{% link messageareas/configuring-a-message-area.md %}) - - [Message networks]({{ site.baseurl }}{% link messageareas/message-networks.md %}) - - [BSO Import & Export]({{ site.baseurl }}{% link messageareas/bso-import-export.md %}) - - [Netmail]({{ site.baseurl }}{% link messageareas/netmail.md %}) - - [QWK]({{ site.baseurl }}{% link messageareas/qwk.md %}) - - [FTN]({{ site.baseurl }}{% link messageareas/ftn.md %}) - - - Art - - [General]({{ site.baseurl }}{% link art/general.md %}) - - [Themes]({{ site.baseurl }}{% link art/themes.md %}) - - [MCI Codes]({{ site.baseurl }}{% link art/mci.md %}) - - - Servers - - Login Servers - - [Telnet]({{ site.baseurl }}{% link servers/telnet.md %}) - - [SSH]({{ site.baseurl }}{% link servers/ssh.md %}) - - [WebSocket]({{ site.baseurl }}{% link servers/websocket.md %}) - - Build your own - - Content Servers - - [Web]({{ site.baseurl }}{% link servers/web-server.md %}) - - [Gopher]({{ site.baseurl }}{% link servers/gopher.md %}) - - [NNTP]({{ site.baseurl }}{% link servers/nntp.md %}) - - - Modding - - [Local Doors]({{ site.baseurl }}{% link modding/local-doors.md %}) - - [Door Servers]({{ site.baseurl }}{% link modding/door-servers.md %}) - - DoorParty - - BBSLink - - Combatnet - - Exodus - - [Telnet Bridge]({{ site.baseurl }}{% link modding/telnet-bridge.md %}) - - [Existing Mods]({{ site.baseurl }}{% link modding/existing-mods.md %}) - - [File Area List]({{ site.baseurl }}{% link modding/file-area-list.md %}) - - [Last Callers]({{ site.baseurl }}{% link modding/last-callers.md %}) - - [Who's Online]({{ site.baseurl }}{% link modding/whos-online.md %}) - - [User List]({{ site.baseurl }}{% link modding/user-list.md %}) - - [Message Conference List]({{ site.baseurl }}{% link modding/msg-conf-list.md %}) - - [Message Area List]({{ site.baseurl }}{% link modding/msg-area-list.md %}) - - [BBS List]({{ site.baseurl }}{% link modding/bbs-list.md %}) - - [Rumorz]({{ site.baseurl }}{% link modding/rumorz.md %}) - - [File Transfer Protocol Select]({{ site.baseurl }}{% link modding/file-transfer-protocol-select.md %}) - - [Onelinerz]({{ site.baseurl }}{% link modding/onelinerz.md %}) - - [Show Art]({{ site.baseurl }}{% link modding/show-art.md %}) - - [Download Manager]({{ site.baseurl }}{% link modding/file-base-download-manager.md %}) - - [Web Download Manager]({{ site.baseurl }}{% link modding/file-base-web-download-manager.md %}) - - [Set Newscan Date]({{ site.baseurl }}{% link modding/set-newscan-date.md %}) - - [Node to Node Messaging]({{ site.baseurl }}{% link modding/node-msg.md %}) - - [Top X]({{ site.baseurl }}{% link modding/top-x.md %}) - - [2FA/OTP Config]({{ site.baseurl }}{% link modding/user-2fa-otp-config.md %}) - - [Auto Signature Editor]({{ site.baseurl }}{% link modding/autosig-edit.md %}) - - - Administration - - [Administration]({{ site.baseurl }}{% link admin/administration.md %}) - - [oputil]({{ site.baseurl }}{% link admin/oputil.md %}) - - [Updating]({{ site.baseurl }}{% link admin/updating.md %}) - - - Troubleshooting - - [Monitoring Logs]({{ site.baseurl }}{% link troubleshooting/monitoring-logs.md %}) diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html index 60f87db8..2066d4c9 100644 --- a/docs/_layouts/default.html +++ b/docs/_layouts/default.html @@ -4,26 +4,55 @@ + + + {% seo %} - Fork me on GitHub + {% if page.include-banner %} + Fork me on GitHub + {% endif %} +
    -
    -
    +
    +
    + + {{ content }} + +
    +
    -
    +
    {% if site.google_analytics %}