From f7e4b577631c380097afe8c63ba1a5d6a8c98d43 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 27 Nov 2020 00:54:56 -0700 Subject: [PATCH 001/146] Gopher server revamp! * gophermap support * More generic and flexible server --- WHATSNEW.md | 1 + core/config_default.js | 2 +- core/servers/content/gopher.js | 59 +++++++++++++++++++++++++++------- docs/servers/gopher.md | 33 ++++++++++++++++--- misc/config_template.in.hjson | 4 +-- misc/gopher_banner.asc | 9 ------ misc/gophermap | 12 +++++++ misc/install.sh | 7 ++++ 8 files changed, 98 insertions(+), 29 deletions(-) delete mode 100644 misc/gopher_banner.asc create mode 100644 misc/gophermap diff --git a/WHATSNEW.md b/WHATSNEW.md index ec186f08..dcf4af4d 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -9,6 +9,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * New `PV` ACS check for arbitrary user properties. See [ACS](./docs/configuration/acs.md) for details. * The `message` arg used by `msg_list` has been deprecated. Please starting using `messageIndex` for this purpose. Support for `message` will be removed in the future. * Added ability to export/download messages. This is enabled in the default menu. See `messageAreaViewPost` in [the default message base template](./misc/menu_templates/message_base.in.hjson) and look for the download options (`@method:addToDownloadQueue`, etc.) for details on adding to your system! +* The Gopher server has had a revamp! Standard `gophermap` files are now served along with any other content you configure for your Gopher Hole! A default [gophermap](https://en.wikipedia.org/wiki/Gopher_(protocol)#Source_code_of_a_menu) can be found [in the misc directory](./misc/gophermap) that behaves like the previous implementation. See [Gopher docs](./docs/servers/gopher.md) for more information. ## 0.0.11-beta * Upgraded from `alpha` to `beta` -- The software is far along and mature enough at this point! diff --git a/core/config_default.js b/core/config_default.js index 82d3eb1d..98aeb4af 100644 --- a/core/config_default.js +++ b/core/config_default.js @@ -263,7 +263,7 @@ module.exports = () => { port : 8070, publicHostname : 'another-fine-enigma-bbs.org', publicPort : 8070, // adjust if behind NAT/etc. - bannerFile : 'gopher_banner.asc', + staticRoot : paths.join(__dirname, './../gopher'), // // Set messageConferences{} to maps of confTag -> [ areaTag1, areaTag2, ... ] diff --git a/core/servers/content/gopher.js b/core/servers/content/gopher.js index 4e51c889..442ecdcb 100644 --- a/core/servers/content/gopher.js +++ b/core/servers/content/gopher.js @@ -27,6 +27,7 @@ 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', @@ -81,8 +82,8 @@ exports.getModule = class GopherModule extends ServerModule { this.publicHostname = config.contentServers.gopher.publicHostname; this.publicPort = config.contentServers.gopher.publicPort; - this.addRoute(/^\/?\r\n$/, this.defaultGenerator); - this.addRoute(/^\/msgarea(\/[a-z0-9_-]+(\/[a-z0-9_-]+)?(\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(_raw)?)?)?\/?\r\n$/, this.messageAreaGenerator); + this.addRoute(/^\/?msgarea(\/[a-z0-9_-]+(\/[a-z0-9_-]+)?(\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(_raw)?)?)?\/?\r\n$/, this.messageAreaGenerator); + this.addRoute(/^(\/?[^\t\r\n]*)\r\n$/, this.staticGenerator); this.server = net.createServer( socket => { socket.setEncoding('ascii'); @@ -161,22 +162,56 @@ exports.getModule = class GopherModule extends ServerModule { return `${itemType}${text}\t${selector}\t${hostname}\t${port}\r\n`; } - defaultGenerator(selectorMatch, cb) { - this.log.debug( { selector : selectorMatch[0] }, 'Serving default content'); + staticGenerator(selectorMatch, cb) { + this.log.debug( { selector : selectorMatch[1] || '(gophermap)' }, 'Serving static content'); - let bannerFile = _.get(Config(), 'contentServers.gopher.bannerFile', 'gopher_banner.asc'); - bannerFile = paths.isAbsolute(bannerFile) ? bannerFile : paths.join(__dirname, '../../../misc', bannerFile); - fs.readFile(bannerFile, 'utf8', (err, banner) => { - if(err) { - return cb('You have reached an ENiGMA½ Gopher server!'); + const requestedPath = selectorMatch[1]; + let path = this.resolveContentPath(requestedPath); + if (!path) { + return cb('Not found'); + } + + fs.stat(path, (err, stats) => { + if (err) { + return cb('Not found'); } - banner = splitTextAtTerms(banner).map(l => this.makeItem(ItemTypes.InfoMessage, l)).join(''); - banner += this.makeItem(ItemTypes.SubMenu, 'Public Message Area', '/msgarea'); - return cb(banner); + let isGopherMap = false; + if (stats.isDirectory()) { + path = paths.join(path, 'gophermap'); + isGopherMap = true; + } + + fs.readFile(path, isGopherMap ? 'utf8' : null, (err, content) => { + if (err) { + let content = 'You have reached an ENiGMA½ Gopher server!\r\n'; + content += this.makeItem(ItemTypes.SubMenu, 'Public Message Area', '/msgarea'); + return cb(content); + } + + if (isGopherMap) { + // Convert any UNIX style LF's to DOS CRLF's + content = content.replace(/\r?\n/g, '\r\n'); + + // variable support + content = content + .replace(/{publicHostname}/g, this.publicHostname) + .replace(/{publicPort}/g, this.publicPort); + } + + return cb(content); + }); }); } + resolveContentPath(requestPath) { + const staticRoot = _.get(Config(), 'contentServers.gopher.staticRoot'); + const path = paths.resolve(staticRoot, `.${requestPath}`); + if (path.startsWith(staticRoot)) { + return path; + } + } + notFoundGenerator(selector, cb) { this.log.debug( { selector }, 'Serving not found content'); return cb('Not found'); diff --git a/docs/servers/gopher.md b/docs/servers/gopher.md index 343c7d80..2877fd80 100644 --- a/docs/servers/gopher.md +++ b/docs/servers/gopher.md @@ -3,7 +3,7 @@ layout: page title: Gopher Server --- ## The Gopher Content Server -The Gopher *content server* provides access to publicly exposed message conferences and areas over Gopher (gopher://). +The Gopher *content server* provides access to publicly exposed message conferences and areas over Gopher (gopher://) as well as any other content you wish to serve in your Gopher Hole! ## Configuration Gopher configuration is found in `contentServers.gopher` in `config.hjson`. @@ -11,14 +11,36 @@ Gopher configuration is found in `contentServers.gopher` in `config.hjson`. | Item | Required | Description | |------|----------|-------------| | `enabled` | :+1: | Set to `true` to enable Gopher | +| `staticRoot` | :+1: | Sets the path serving as the static root path for all Gopher content. Defaults to `enigma-bbs/gopher`.
See also **Gophermap's** below | | `port` | :-1: | Override the default port of `8070` | -| `publicHostName` | :+1: | Set the **public** hostname/domain that Gopher will serve to the outside world. Example: `myfancybbs.com` | +| `publicHostname` | :+1: | Set the **public** hostname/domain that Gopher will serve to the outside world. Example: `myfancybbs.com` | | `publicPort` | :+1: | Set the **public** port that Gopher will serve to the outside world. | -| `messageConferences` | :+1: | An map of *conference tags* to *area tags* that are publicly exposed via Gopher. See example below. | +| `messageConferences` | :-1: | An map of *conference tags* to *area tags* that are publicly exposed via Gopher. See example below. | -Notes on `publicHostName` and `publicPort`: +Notes on `publicHostname` and `publicPort`: The Gopher protocol serves content that contains host/domain and port even when referencing it's own documents. Due to this, these members must be set to your publicly addressable Gopher server! +## Gophermap's +[Gophermap's](https://en.wikipedia.org/wiki/Gopher_(protocol)#Source_code_of_a_menu) are how to build menus for your Gopher Hole. Each map is a simple text file named `gophermap` (all lowercase, no extension) with DOS style CRLF endings. + +Within any directory nested within your `staticRoot` may live a `gophermap`. A template may be found in the `enigma-bbsmisc` directory. + +ENiGMA will pre-process `gophermap` files replacing in following variables: +* `{publicHostname}`: The public hostname from your config. +* `{publicPort}`: The public port from your config. + +:information_source: See [Wikipedia](https://en.wikipedia.org/wiki/Gopher_(protocol)#Source_code_of_a_menu) for more information on the `gophermap` format. + +:bulb: Tools such as [gfu](https://rawtext.club/~sloum/gfu.html) may help you with `gophermap`'s + +### Example Gophermap +An example `gophermap` living in `enigma-bbs/gopher`: +``` +iWelcome to a Gopher server! {publicHostname} {publicPort} +1Public Message Area /msgarea {publicHostname} {publicPort} +. +``` + ### Example Let's suppose you are serving Gopher for your BBS at `myfancybbs.com`. Your ENiGMA½ system is listening on the default Gopher `port` of 8070 but you're behind a firewall and want port 70 exposed to the public. Lastly, you want to expose some fsxNet areas: @@ -26,9 +48,10 @@ Let's suppose you are serving Gopher for your BBS at `myfancybbs.com`. Your ENiG contentServers: { gopher: { enabled: true - publicHostName: myfancybbs.com + publicHostname: myfancybbs.com publicPort: 70 + // Expose some public message conferences/areas messageConferences: { fsxnet: { // fsxNet's conf tag // Areas of fsxNet we want to expose: diff --git a/misc/config_template.in.hjson b/misc/config_template.in.hjson index 1db5406d..21cefeb6 100644 --- a/misc/config_template.in.hjson +++ b/misc/config_template.in.hjson @@ -242,8 +242,8 @@ port: XXXXX enabled: false - // bannerFile path in misc/ by default. Full paths allowed. - bannerFile: XXXXX + // The root directory to serve gophermaps and other content + staticRoot: XXXXX // // The Gopher Content Server can export message base diff --git a/misc/gopher_banner.asc b/misc/gopher_banner.asc deleted file mode 100644 index b758e066..00000000 --- a/misc/gopher_banner.asc +++ /dev/null @@ -1,9 +0,0 @@ -_____________________ _____ ____________________ __________\_ / -\__ ____/\_ ____ \ /____/ / _____ __ \ / ______/ // /___jp! -// __|___// | \// |// | \// | | \// \ /___ /_____ -/____ _____| __________ ___|__| ____| \ / _____ \ ----- \______\ -- |______\ ------ /______/ ---- |______\ - |______\ /__/ // ___/ - /__ _\ - <*> ENiGMA½ // HTTPS://GITHUB.COM/NUSKOOLER/ENIGMA-BBS <*> /__/ - -------------------------------------------------------------------------------- diff --git a/misc/gophermap b/misc/gophermap new file mode 100644 index 00000000..c4973670 --- /dev/null +++ b/misc/gophermap @@ -0,0 +1,12 @@ +i_____________________ _____ ____________________ __________\_ / +i\__ ____/\_ ____ \ /____/ / _____ __ \ / ______/ // /___jp! +i// __|___// | \// |// | \// | | \// \ /___ /_____ +i/____ _____| __________ ___|__| ____| \ / _____ \ +i---- \______\ -- |______\ ------ /______/ ---- |______\ - |______\ /__/ // ___/ +i /__ _\ +i <*> ENiGMA½ // HTTPS://GITHUB.COM/NUSKOOLER/ENIGMA-BBS <*> /__/ +i +i------------------------------------------------------------------------------- +i +1Public Message Area /msgarea {publicHostname} {publicPort} +. diff --git a/misc/install.sh b/misc/install.sh index 741c4585..9a894a37 100755 --- a/misc/install.sh +++ b/misc/install.sh @@ -146,6 +146,12 @@ install_node_packages() { fi } +copy_template_files() { + if [[ ! -f "./gopher/gophermap" ]]; then + cp "./misc/gophermap" "./gopher/gophermap" + fi +} + enigma_footer() { log "ENiGMA½ installation complete!" echo -e "\e[1;33m" @@ -189,6 +195,7 @@ install_nvm configure_nvm download_enigma_source install_node_packages +copy_template_files enigma_footer } # this ensures the entire script is downloaded before execution From 18bfee58b85be5fc747a6a4631a2f13db58a8d45 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 27 Nov 2020 01:03:12 -0700 Subject: [PATCH 002/146] Add gopher/README --- gopher/README | 1 + 1 file changed, 1 insertion(+) create mode 100644 gopher/README diff --git a/gopher/README b/gopher/README new file mode 100644 index 00000000..9f773a65 --- /dev/null +++ b/gopher/README @@ -0,0 +1 @@ +Copy ../misc/gophermap to this directory and edit to your liking! From 94a34a33110f1df1feb7760755efcd8ccb57290a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 27 Nov 2020 01:04:47 -0700 Subject: [PATCH 003/146] Note on RFC --- docs/servers/gopher.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/servers/gopher.md b/docs/servers/gopher.md index 2877fd80..03c34bed 100644 --- a/docs/servers/gopher.md +++ b/docs/servers/gopher.md @@ -31,6 +31,8 @@ ENiGMA will pre-process `gophermap` files replacing in following variables: :information_source: See [Wikipedia](https://en.wikipedia.org/wiki/Gopher_(protocol)#Source_code_of_a_menu) for more information on the `gophermap` format. +:information_source: See [RFC 1436](https://tools.ietf.org/html/rfc1436) for the original Gopher spec. + :bulb: Tools such as [gfu](https://rawtext.club/~sloum/gfu.html) may help you with `gophermap`'s ### Example Gophermap From cd3b495e6c78c6038dc67e7381b9ed1a911781dc Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 27 Nov 2020 12:50:57 -0700 Subject: [PATCH 004/146] SECURITY FIX * Do not allow relative paths to route outside of www static root area --- core/servers/content/web.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/core/servers/content/web.js b/core/servers/content/web.js index 04b9ccdd..70ede1c1 100644 --- a/core/servers/content/web.js +++ b/core/servers/content/web.js @@ -215,20 +215,22 @@ exports.getModule = class WebServerModule extends ServerModule { routeIndex(req, resp) { const filePath = paths.join(Config().contentServers.web.staticRoot, 'index.html'); - return this.returnStaticPage(filePath, resp); } routeStaticFile(req, resp) { const fileName = req.url.substr(req.url.indexOf('/', 1)); - const filePath = paths.join(Config().contentServers.web.staticRoot, fileName); - + const filePath = this.resolveStaticPath(fileName); return this.returnStaticPage(filePath, resp); } returnStaticPage(filePath, resp) { const self = this; + if (!filePath) { + return this.fileNotFound(resp); + } + fs.stat(filePath, (err, stats) => { if(err || !stats.isFile()) { return self.fileNotFound(resp); @@ -245,6 +247,14 @@ exports.getModule = class WebServerModule extends ServerModule { }); } + resolveStaticPath(requestPath) { + const staticRoot = _.get(Config(), 'contentServers.web.staticRoot'); + const path = paths.resolve(staticRoot, `.${requestPath}`); + if (path.startsWith(staticRoot)) { + return path; + } + } + routeTemplateFilePage(templatePath, preprocessCallback, resp) { const self = this; From d55e8fb3e05bd94b1b5a61d4cc6fb24a18058e80 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 27 Nov 2020 16:44:40 -0700 Subject: [PATCH 005/146] Let users scroll desc in file browser --- WHATSNEW.md | 1 + art/themes/luciano_blocktronics/FBHELP.ANS | Bin 664 -> 663 bytes art/themes/luciano_blocktronics/FBRWSE.ANS | Bin 899 -> 1005 bytes core/file_area_list.js | 22 ++++++++++++++++++++- misc/menu_templates/file_base.in.hjson | 5 +++++ 5 files changed, 27 insertions(+), 1 deletion(-) diff --git a/WHATSNEW.md b/WHATSNEW.md index dcf4af4d..f6dd1fc0 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -10,6 +10,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * The `message` arg used by `msg_list` has been deprecated. Please starting using `messageIndex` for this purpose. Support for `message` will be removed in the future. * Added ability to export/download messages. This is enabled in the default menu. See `messageAreaViewPost` in [the default message base template](./misc/menu_templates/message_base.in.hjson) and look for the download options (`@method:addToDownloadQueue`, etc.) for details on adding to your system! * The Gopher server has had a revamp! Standard `gophermap` files are now served along with any other content you configure for your Gopher Hole! A default [gophermap](https://en.wikipedia.org/wiki/Gopher_(protocol)#Source_code_of_a_menu) can be found [in the misc directory](./misc/gophermap) that behaves like the previous implementation. See [Gopher docs](./docs/servers/gopher.md) for more information. +* Default file browser up/down/pageUp/pageDown scrolls description (e.g. FILE_ID.DIZ) ## 0.0.11-beta * Upgraded from `alpha` to `beta` -- The software is far along and mature enough at this point! diff --git a/art/themes/luciano_blocktronics/FBHELP.ANS b/art/themes/luciano_blocktronics/FBHELP.ANS index 5dc95322fdf79ea5b521cea023e5e6c61c138728..08ea5885f343a4d8958204e84adc8c2a855f3871 100644 GIT binary patch delta 127 zcmbQiI-PYwUXX%xv|+BCymYj&L9RZKG|w$a4=q69y5yJVfd%w{0I1&BEVo!X+Q1q} yCl}@CCHa5%6NX$!5RRBq*K}9lCb5e>Y s9`Fz~Gz4lgHqV9WmX0=ewg>8+tj8F`Z3>e!G@Lw<@d2aCf6G3>TGYhCSvuO#+Sn{tK|w(}+8|dt+Q@XGt%R@3=7h;bOs^SRCdVa0% delta 55 zcmaFM-poG1dNL<7@5E?cCJX0@)t1brCe9mABrr}s%qTY5pGghGpWMl0&uBXN4%2H! JsmUjpRRPvC5bXc} diff --git a/core/file_area_list.js b/core/file_area_list.js index a62271ca..12abfd0d 100644 --- a/core/file_area_list.js +++ b/core/file_area_list.js @@ -144,7 +144,10 @@ exports.getModule = class FileAreaList extends MenuModule { }, displayHelp : (formData, extraArgs, cb) => { return this.displayHelpPage(cb); - } + }, + movementKeyPressed : (formData, extraArgs, cb) => { + return this._handleMovementKeyPress(_.get(formData, 'key.name'), cb); + }, }; } @@ -505,6 +508,23 @@ exports.getModule = class FileAreaList extends MenuModule { ); } + _handleMovementKeyPress(keyName, cb) { + const descView = this.viewControllers.browse.getView(MciViewIds.browse.desc); + if (!descView) { + return cb(null); + } + + switch (keyName) { + case 'down arrow' : descView.scrollDocumentUp(); break; + case 'up arrow' : descView.scrollDocumentDown(); break; + case 'page up' : descView.keyPressPageUp(); break; + case 'page down' : descView.keyPressPageDown(); break; + } + + this.viewControllers.browse.switchFocus(MciViewIds.browse.navMenu); + return cb(null); + } + fetchAndDisplayWebDownloadLink(cb) { const self = this; diff --git a/misc/menu_templates/file_base.in.hjson b/misc/menu_templates/file_base.in.hjson index 572abf97..afd497d0 100644 --- a/misc/menu_templates/file_base.in.hjson +++ b/misc/menu_templates/file_base.in.hjson @@ -73,6 +73,7 @@ mci: { MT1: { mode: preview + acceptsFocus: false } HM2: { @@ -152,6 +153,10 @@ keys: [ "?" ] action: @method:displayHelp } + { + keys: [ "down arrow", "up arrow", "page up", "page down" ] + action: @method:movementKeyPressed + } ] } From b65056f3155c50431ef8daf2e4e7a4043cd87233 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 27 Nov 2020 16:49:34 -0700 Subject: [PATCH 006/146] Upgrade note --- WHATSNEW.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WHATSNEW.md b/WHATSNEW.md index f6dd1fc0..9579c562 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -10,7 +10,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * The `message` arg used by `msg_list` has been deprecated. Please starting using `messageIndex` for this purpose. Support for `message` will be removed in the future. * Added ability to export/download messages. This is enabled in the default menu. See `messageAreaViewPost` in [the default message base template](./misc/menu_templates/message_base.in.hjson) and look for the download options (`@method:addToDownloadQueue`, etc.) for details on adding to your system! * The Gopher server has had a revamp! Standard `gophermap` files are now served along with any other content you configure for your Gopher Hole! A default [gophermap](https://en.wikipedia.org/wiki/Gopher_(protocol)#Source_code_of_a_menu) can be found [in the misc directory](./misc/gophermap) that behaves like the previous implementation. See [Gopher docs](./docs/servers/gopher.md) for more information. -* Default file browser up/down/pageUp/pageDown scrolls description (e.g. FILE_ID.DIZ) +* Default file browser up/down/pageUp/pageDown scrolls description (e.g. FILE_ID.DIZ). If you want to expose this on an existing system see the `fileBaseListEntries` in the default `file_base.in.hjson` template. ## 0.0.11-beta * Upgraded from `alpha` to `beta` -- The software is far along and mature enough at this point! From 89446d26e9400f437b390629083940bda9f701b7 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 29 Nov 2020 11:45:36 -0700 Subject: [PATCH 007/146] Working pretty well --- core/file_entry.js | 95 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 81 insertions(+), 14 deletions(-) diff --git a/core/file_entry.js b/core/file_entry.js index 0539f137..90a9bac2 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -457,6 +457,16 @@ module.exports = class FileEntry { ); } + // + // Find file(s) by |filter| + // + // - sort: sort results by any well known name, file_id, or user_rating + // - terms: one or more search terms to search within filenames as well + // as short and long descriptions. We attempt to use the FTS ability when + // possible, but want to allow users to search for wildcard matches in + // which some cases we'll use multiple LIKE queries. + // See _normalizeFileSearchTerms() + // static findFiles(filter, cb) { filter = filter || {}; @@ -500,8 +510,8 @@ module.exports = class FileEntry { if('user_rating' === filter.sort) { sql = `SELECT DISTINCT f.file_id, - (SELECT IFNULL(AVG(rating), 0) rating - FROM file_user_rating + (SELECT IFNULL(AVG(rating), 0) rating + FROM file_user_rating WHERE file_id = f.file_id) AS avg_rating FROM file f`; @@ -540,16 +550,16 @@ module.exports = class FileEntry { mp.value = mp.value.replace(/\*/g, '%').replace(/\?/g, '_'); appendWhereClause( `f.file_id IN ( - SELECT file_id - FROM file_meta + SELECT file_id + FROM file_meta WHERE meta_name = "${mp.name}" AND meta_value LIKE "${mp.value}" )` ); } else { appendWhereClause( `f.file_id IN ( - SELECT file_id - FROM file_meta + SELECT file_id + FROM file_meta WHERE meta_name = "${mp.name}" AND meta_value = "${mp.value}" )` ); @@ -562,14 +572,29 @@ module.exports = class FileEntry { } if(filter.terms && filter.terms.length > 0) { - // note the ':' in MATCH expr., see https://www.sqlite.org/cvstrac/wiki?p=FullTextIndex - appendWhereClause( - `f.file_id IN ( - SELECT rowid - FROM file_fts - WHERE file_fts MATCH ":${sanitizeString(filter.terms)}" - )` - ); + const [terms, queryType] = FileEntry._normalizeFileSearchTerms(filter.terms); + + if ('fts_match' === queryType) { + // note the ':' in MATCH expr., see https://www.sqlite.org/cvstrac/wiki?p=FullTextIndex + appendWhereClause( + `f.file_id IN ( + SELECT rowid + FROM file_fts + WHERE file_fts MATCH ":${terms}" + )` + ); + } else { + appendWhereClause( + `(f.file_name LIKE "${terms}" OR + f.desc LIKE "${terms}" OR + f.desc_long LIKE "${terms}")` + ); + } + } + + // handle e.g. 1998 -> "1998" + if (_.isNumber(filter.tags)) { + filter.tags = filter.tags.toString(); } if(filter.tags && filter.tags.length > 0) { @@ -693,4 +718,46 @@ module.exports = class FileEntry { } ); } + + static _normalizeFileSearchTerms(terms) { + // ensure we have reasonable input to start with + terms = sanitizeString(terms.toString()); + + // No wildcards? + const hasSingleCharWC = terms.indexOf('?') > -1; + if (terms.indexOf('*') === -1 && !hasSingleCharWC) { + return [ terms, 'fts_match' ]; + } + + const prepareLike = () => { + // Convert * and ? to SQL LIKE style + terms = terms.replace(/\*/g, '%').replace(/\?/g, '_'); + return terms; + }; + + // Any ? wildcards? + if (hasSingleCharWC) { + return [ prepareLike(terms), 'like' ]; + } + + const split = terms.replace(/\s+/g, ' ').split(' '); + const useLike = split.some(term => { + if (term.indexOf('?') > -1) { + return true; + } + + const wcPos = term.indexOf('*'); + if (wcPos > -1 && wcPos !== term.length - 1) { + return true; + } + + return false; + }); + + if (useLike) { + return [ prepareLike(terms), 'like' ]; + } + + return [ terms, 'fts_match' ]; + } }; From 677645300586a9aeb1d4474db8c979487b96e661 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 29 Nov 2020 14:30:18 -0700 Subject: [PATCH 008/146] File search containing a single " causes lockup #332 --- core/file_area_list.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/file_area_list.js b/core/file_area_list.js index 12abfd0d..637c3c3e 100644 --- a/core/file_area_list.js +++ b/core/file_area_list.js @@ -204,7 +204,7 @@ exports.getModule = class FileAreaList extends MenuModule { }, function display(callback) { return self.displayBrowsePage(false, err => { - if(err && 'NORESULTS' === err.reasonCode) { + if(err) { self.gotoMenu(self.menuConfig.config.noResultsMenu || 'fileBaseListEntriesNoResults'); } return callback(err); @@ -740,7 +740,7 @@ exports.getModule = class FileAreaList extends MenuModule { } FileEntry.findFiles(filterCriteria, (err, fileIds) => { - this.fileList = fileIds; + this.fileList = fileIds || []; return cb(err); }); } From 6f06821f0c3d45e3938504bf3476ecb379cf2450 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 29 Nov 2020 14:43:26 -0700 Subject: [PATCH 009/146] oputil cannot remove user from group #331 --- WHATSNEW.md | 2 ++ core/oputil/oputil_help.js | 2 +- core/oputil/oputil_user.js | 6 +++--- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/WHATSNEW.md b/WHATSNEW.md index 9579c562..f03b6cee 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -11,6 +11,8 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * Added ability to export/download messages. This is enabled in the default menu. See `messageAreaViewPost` in [the default message base template](./misc/menu_templates/message_base.in.hjson) and look for the download options (`@method:addToDownloadQueue`, etc.) for details on adding to your system! * The Gopher server has had a revamp! Standard `gophermap` files are now served along with any other content you configure for your Gopher Hole! A default [gophermap](https://en.wikipedia.org/wiki/Gopher_(protocol)#Source_code_of_a_menu) can be found [in the misc directory](./misc/gophermap) that behaves like the previous implementation. See [Gopher docs](./docs/servers/gopher.md) for more information. * Default file browser up/down/pageUp/pageDown scrolls description (e.g. FILE_ID.DIZ). If you want to expose this on an existing system see the `fileBaseListEntries` in the default `file_base.in.hjson` template. +* File base search has had an improvement to search term handling. +* `./oputil user group -group` to now accepts `~group` removing the need for special handling of the "-" character. #331 ## 0.0.11-beta * Upgraded from `alpha` to `beta` -- The software is far along and mature enough at this point! diff --git a/core/oputil/oputil_help.js b/core/oputil/oputil_help.js index 50bc3b5e..05025cfe 100644 --- a/core/oputil/oputil_help.js +++ b/core/oputil/oputil_help.js @@ -57,7 +57,7 @@ Actions: lock USERNAME Set a user's status to "locked" - group USERNAME [+|-]GROUP Adds (+) or removes (-) user from a group + group USERNAME [+|~]GROUP Adds (+) or removes (~) user from a group list [FILTER] List users with optional FILTER. diff --git a/core/oputil/oputil_user.js b/core/oputil/oputil_user.js index 403c5076..7c71523f 100644 --- a/core/oputil/oputil_user.js +++ b/core/oputil/oputil_user.js @@ -275,7 +275,7 @@ function modUserGroups(user) { let groupName = argv._[argv._.length - 1].toString().replace(/["']/g, ''); // remove any quotes - necessary to allow "-foo" let action = groupName[0]; // + or - - if('-' === action || '+' === action) { + if('-' === action || '+' === action || '~' === action) { groupName = groupName.substr(1); } @@ -286,7 +286,7 @@ function modUserGroups(user) { } // - // Groups are currently arbritary, so do a slight validation + // Groups are currently arbitrary, so do a slight validation // if(!/[A-Za-z0-9]+/.test(groupName)) { process.exitCode = ExitCodes.BAD_ARGS; @@ -303,7 +303,7 @@ function modUserGroups(user) { } const UserGroup = require('../../core/user_group.js'); - if('-' === action) { + if('-' === action || '~' === action) { UserGroup.removeUserFromGroup(user.userId, groupName, done); } else { UserGroup.addUserToGroup(user.userId, groupName, done); From 4d42eeb3185a29afdfe357a733cfbe071327db57 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 29 Nov 2020 14:45:24 -0700 Subject: [PATCH 010/146] Fix up docs --- docs/admin/oputil.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/admin/oputil.md b/docs/admin/oputil.md index 49e7039c..b2ee2892 100644 --- a/docs/admin/oputil.md +++ b/docs/admin/oputil.md @@ -76,7 +76,16 @@ Actions: lock USERNAME Set a user's status to "locked" - group USERNAME [+|-]GROUP Adds (+) or removes (-) user from a group + group USERNAME [+|~]GROUP Adds (+) or removes (~) user from a group + + list [FILTER] List users with optional FILTER. + + Valid filters: + all : All users (default). + disabled : Disabled users. + inactive : Inactive users. + active : Active (regular) users. + locked : Locked users. info arguments: --security Include security information in output @@ -104,7 +113,7 @@ info arguments: | `deactivate` | Deactivates user | `./oputil.js user deactivate joeuser` | N/A | | `disable` | Disables user (user will not be able to login) | `./oputil.js user disable joeuser` | N/A | | `lock` | Locks the user account (prevents logins) | `./oputil.js user lock joeuser` | N/A | -| `group` | Modifies users group membership | Add to group: `./oputil.js user group joeuser +derp`
Remove from group: `./oputil.js user group joeuser -derp` | N/A | +| `group` | Modifies users group membership | Add to group: `./oputil.js user group joeuser +derp`
Remove from group: `./oputil.js user group joeuser ~derp` | N/A | #### Manage 2FA/OTP While `oputil.js` can be used to manage a user's 2FA/OTP, it is highly recommended to require users to opt-in themselves. See [Security](../configuration/security.md) for details. From e05e8a2e35b6f5b8cb5126971178a37068e63bb5 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 29 Nov 2020 16:24:51 -0700 Subject: [PATCH 011/146] Initial --- core/database.js | 11 ++++++++--- core/file_entry.js | 9 +++++++++ core/oputil/oputil_user.js | 1 + 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/core/database.js b/core/database.js index 17bc3844..55ed4e2e 100644 --- a/core/database.js +++ b/core/database.js @@ -421,7 +421,8 @@ const DB_INIT_TABLE = { hash_tag_id INTEGER NOT NULL, file_id INTEGER NOT NULL, - UNIQUE(hash_tag_id, file_id) + UNIQUE(hash_tag_id, file_id), + FOREIGN KEY(file_id) REFERENCES file(file_id) ON DELETE CASCADE );` ); @@ -431,7 +432,10 @@ const DB_INIT_TABLE = { user_id INTEGER NOT NULL, rating INTEGER NOT NULL, - UNIQUE(file_id, user_id) + UNIQUE(file_id, user_id), + FOREIGN KEY(file_id) REFERENCES file(file_id) ON DELETE CASCADE + -- Note that we cannot CASCADE if user_id is removed from user.db + -- See processing in oputil's removeUser() );` ); @@ -447,7 +451,8 @@ const DB_INIT_TABLE = { hash_id VARCHAR NOT NULL, file_id INTEGER NOT NULL, - UNIQUE(hash_id, file_id) + UNIQUE(hash_id, file_id), + FOREIGN KEY(file_id) REFERENCES file(file_id) ON DELETE CASCADE );` ); diff --git a/core/file_entry.js b/core/file_entry.js index 90a9bac2..476864ca 100644 --- a/core/file_entry.js +++ b/core/file_entry.js @@ -243,6 +243,15 @@ module.exports = class FileEntry { ); } + static removeUserRatings(userId, cb) { + return fileDb.run( + `DELETE FROM file_user_rating + WHERE user_id = ?;`, + [ userId ], + cb + ); + } + static persistMetaValue(fileId, name, value, transOrDb, cb) { if(!_.isFunction(cb) && _.isFunction(transOrDb)) { cb = transOrDb; diff --git a/core/oputil/oputil_user.js b/core/oputil/oputil_user.js index 7c71523f..d3a13931 100644 --- a/core/oputil/oputil_user.js +++ b/core/oputil/oputil_user.js @@ -172,6 +172,7 @@ function removeUser(user) { message : [ 'user_message_area_last_read' ], system : [ 'user_event_log', ], user : [ 'user_group_member', 'user' ], + file : [ 'file_user_rating'] }; async.eachSeries(Object.keys(DeleteFrom), (dbName, nextDbName) => { From c87ac8680aa455d5a967240cca87f7745c5d27b1 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 29 Nov 2020 16:47:03 -0700 Subject: [PATCH 012/146] Notes + script for update --- UPGRADE.md | 7 ++++ WHATSNEW.md | 1 + misc/update/tables_update_2020-11-29.sql | 41 ++++++++++++++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 misc/update/tables_update_2020-11-29.sql diff --git a/UPGRADE.md b/UPGRADE.md index 117b4bff..f87c2079 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -39,6 +39,13 @@ Report your issue on Xibalba BBS, hop in #enigma-bbs on FreeNode and chat, or ] } ``` +* A set of database fixes were made that cause some records to be properly cleaned up when e.g. deleting a file. Existing `file.db` databases will need to be updated **manually**. Note that this applies to users upgrading within 0.0.12-beta as well: +1. **Make a backup of your file.db!** +2. Shut down ENiGMA. +3. From the enigma-bbs directory: +``` +sqlite3 db/file.sqlite3 < ./misc/update/tables_update_2020-11-29.sql +``` # 0.0.10-alpha to 0.0.11-beta * Node.js 12.x LTS is now in use. Follow standard Node.js upgrade procedures (e.g.: `nvm install 12 && nvm use 12`). diff --git a/WHATSNEW.md b/WHATSNEW.md index f03b6cee..ee0e4943 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -13,6 +13,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * Default file browser up/down/pageUp/pageDown scrolls description (e.g. FILE_ID.DIZ). If you want to expose this on an existing system see the `fileBaseListEntries` in the default `file_base.in.hjson` template. * File base search has had an improvement to search term handling. * `./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`). ## 0.0.11-beta * Upgraded from `alpha` to `beta` -- The software is far along and mature enough at this point! diff --git a/misc/update/tables_update_2020-11-29.sql b/misc/update/tables_update_2020-11-29.sql new file mode 100644 index 00000000..902423dc --- /dev/null +++ b/misc/update/tables_update_2020-11-29.sql @@ -0,0 +1,41 @@ +PRAGMA foreign_keys=OFF; + +BEGIN; + +CREATE TABLE IF NOT EXISTS file_hash_tag_new ( + hash_tag_id INTEGER NOT NULL, + file_id INTEGER NOT NULL, + + UNIQUE(hash_tag_id, file_id), + FOREIGN KEY(file_id) REFERENCES file(file_id) ON DELETE CASCADE +); +INSERT INTO file_hash_tag_new SELECT * FROM file_hash_tag; +DROP TABLE file_hash_tag; +ALTER TABLE file_hash_tag_new RENAME TO file_hash_tag; + +CREATE TABLE IF NOT EXISTS file_user_rating_new ( + file_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + rating INTEGER NOT NULL, + + UNIQUE(file_id, user_id), + FOREIGN KEY(file_id) REFERENCES file(file_id) ON DELETE CASCADE +); +INSERT INTO file_user_rating_new SELECT * FROM file_user_rating; +DROP TABLE file_user_rating; +ALTER TABLE file_user_rating_new RENAME TO file_user_rating; + +CREATE TABLE IF NOT EXISTS file_web_serve_batch_new ( + hash_id VARCHAR NOT NULL, + file_id INTEGER NOT NULL, + + UNIQUE(hash_id, file_id), + FOREIGN KEY(file_id) REFERENCES file(file_id) ON DELETE CASCADE +); +INSERT INTO file_web_serve_batch_new SELECT * FROM file_web_serve_batch; +DROP TABLE file_web_serve_batch; +ALTER TABLE file_web_serve_batch_new RENAME TO file_web_serve_batch; + +PRAGMA foreign_key_check; +COMMIT; +PRAGMA foreign_keys=ON; \ No newline at end of file From 1afd1ef04182d972c2a83e594c5b8ffa42db6bef Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 29 Nov 2020 16:53:10 -0700 Subject: [PATCH 013/146] Newline --- misc/update/tables_update_2020-11-29.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/update/tables_update_2020-11-29.sql b/misc/update/tables_update_2020-11-29.sql index 902423dc..7d8ac862 100644 --- a/misc/update/tables_update_2020-11-29.sql +++ b/misc/update/tables_update_2020-11-29.sql @@ -38,4 +38,4 @@ ALTER TABLE file_web_serve_batch_new RENAME TO file_web_serve_batch; PRAGMA foreign_key_check; COMMIT; -PRAGMA foreign_keys=ON; \ No newline at end of file +PRAGMA foreign_keys=ON; From 76aff0e5e1dd8ea78d3708cf342868cfb6ff85e4 Mon Sep 17 00:00:00 2001 From: Tony Toon Date: Sun, 6 Dec 2020 07:43:28 -0600 Subject: [PATCH 014/146] Update abracadabra.js fix for next parameter in abracadabra. allows usage of doors in login/etc. without entering infinite loop. --- core/abracadabra.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/abracadabra.js b/core/abracadabra.js index 83aa376b..b2e641e1 100644 --- a/core/abracadabra.js +++ b/core/abracadabra.js @@ -199,7 +199,7 @@ exports.getModule = class AbracadabraModule extends MenuModule { '\r\n\r\n' ); - this.prevMenu(); + this.autoNextMenu(); }); } From b579d966b601dab9bfd7e098081f53f1cfa44361 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 7 Dec 2020 19:53:49 -0700 Subject: [PATCH 015/146] Allow suffix on onelinerz DB name --- WHATSNEW.md | 1 + core/onelinerz.js | 7 ++++--- docs/modding/onelinerz.md | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/WHATSNEW.md b/WHATSNEW.md index ee0e4943..057ff329 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -14,6 +14,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * File base search has had an improvement to search term handling. * `./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. ## 0.0.11-beta * Upgraded from `alpha` to `beta` -- The software is far along and mature enough at this point! diff --git a/core/onelinerz.js b/core/onelinerz.js index d840f731..679f5fec 100644 --- a/core/onelinerz.js +++ b/core/onelinerz.js @@ -135,7 +135,7 @@ exports.getModule = class OnelinerzModule extends MenuModule { self.db.each( `SELECT * FROM ( - SELECT * + SELECT * FROM onelinerz ORDER BY timestamp DESC LIMIT ${limit} @@ -248,8 +248,9 @@ exports.getModule = class OnelinerzModule extends MenuModule { async.series( [ function openDatabase(callback) { + const dbSuffix = self.menuConfig.config.dbSuffix; self.db = getTransactionDatabase(new sqlite3.Database( - getModDatabasePath(exports.moduleInfo), + getModDatabasePath(exports.moduleInfo, dbSuffix), err => { return callback(err); } @@ -260,7 +261,7 @@ exports.getModule = class OnelinerzModule extends MenuModule { `CREATE TABLE IF NOT EXISTS onelinerz ( id INTEGER PRIMARY KEY, user_id INTEGER_NOT NULL, - user_name VARCHAR NOT NULL, + user_name VARCHAR NOT NULL, oneliner VARCHAR NOT NULL, timestamp DATETIME NOT NULL );` diff --git a/docs/modding/onelinerz.md b/docs/modding/onelinerz.md index 92515617..001c4e04 100644 --- a/docs/modding/onelinerz.md +++ b/docs/modding/onelinerz.md @@ -9,6 +9,7 @@ The built in `onelinerz` module provides a retro onelinerz system. ### Config Block Available `config` block entries: * `dateTimeFormat`: [moment.js](https://momentjs.com) style format. Defaults to current theme → system `short` date format. +* `dbSuffix`: Provide a suffix that will be appended to the DB name to use onelinerz for more than one purpose (separate lists). ### Theming The following `itemFormat` object is provided to MCI 1 (ie: `%VM1`): From 552982187673d6be87d45fa42113a173ce2eed3e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 8 Dec 2020 17:28:22 -0700 Subject: [PATCH 016/146] Update README.md Add discussions link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ca3b7412..188f3ec0 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ If you feel the urge to donate, [you can do so here](https://liberapay.com/NuSko Donate using Liberapay ## Support -* Use [the issue tracker](https://github.com/NuSkooler/enigma-bbs/issues) +* See [Discussions](https://github.com/NuSkooler/enigma-bbs/discussions) and [the issue tracker](https://github.com/NuSkooler/enigma-bbs/issues) * **Discussion on a ENiGMA BBS!** (see Boards below) * IRC: **#enigma-bbs** on **chat.freenode.net** ([webchat](https://webchat.freenode.net/?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 From 6bae3f70500da7b2c58e31ce5387d56a2ad97f73 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 9 Dec 2020 19:31:51 -0700 Subject: [PATCH 017/146] More doc updates on menu.hjson and such --- docs/configuration/menu-hjson.md | 54 ++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/docs/configuration/menu-hjson.md b/docs/configuration/menu-hjson.md index 0c409d2f..ab935222 100644 --- a/docs/configuration/menu-hjson.md +++ b/docs/configuration/menu-hjson.md @@ -3,13 +3,15 @@ layout: page title: Menu HSJON --- ## Menu HJSON -The core of a ENiGMA½ based BBS is `menu.hjson`. Note that when `menu.hjson` is referenced, we're actually talking about `config/menus/yourboardname-*.hjson`. These files determines the menus (or screens) a user can see, the order they come in, how they interact with each other, ACS configuration, and so on. Like all configuration within ENiGMA½, menu configuration is done in [HJSON](https://hjson.org/) format. See [HJSON General Information](hjson.md) for more information. +The core of a ENiGMA½ based BBS is it's menus driven by what will be referred to as `menu.hjson`. Throughout ENiGMA½ documentation, when `menu.hjson` is referenced, we're actually talking about `config/menus/yourboardname-*.hjson`. These files determine the menus (or screens) a user can see, the order they come in, how they interact with each other, ACS configuration, and so on. Like all configuration within ENiGMA½, menu configuration is done in [HJSON](https://hjson.org/) format. -Entries in `menu.hjson` are often referred to as *blocks* or *sections*. Each entry defines a menu. A menu in this sense is something the user can see or visit. Examples include but are not limited to: +:information_source: See also [HJSON General Information](hjson.md) for more information on the HJSON file format. -* Classical Main, Messages, and File menus -* Art file display -* Module driven menus such as door launchers and other custom mods +:bulb: Entries in `menu.hjson` are often referred to as *blocks* or *sections*. Each entry defines a menu. A menu in this sense is something the user can see or visit. Examples include but are not limited to: + +* Classical navigation and menus such as Main, Messages, and Files. +* Art file display. +* Module driven menus such as [door launchers](../modding/local-doors.md), [Onelinerz](../modding/onelinzerz.md), and other custom mods. Menu entries live under the `menus` section of `menu.hjson`. The *key* for a menu is it's name that can be referenced by other menus and areas of the system. @@ -25,24 +27,25 @@ As you can see a menu can be very simple. :information_source: Remember that the top level menu may include additional files using the `includes` directive. See [Configuration Files](config-files.md) for more information on this. ## Common Menu Entry Members -Below is a table of **common** menu entry members. These members apply to most entries, though entries that are backed by a specialized module (ie: `module: bbs_list`) may differ. See documentation for the module in question for particulars. +Below is a table of **common** menu entry members. These members apply to most entries, though entries that are backed by a specialized module (ie: `module: bbs_list`) may differ. Menus that use their own module contain a `module` declaration: + +```hjson +module: some_fancy_module +``` + +See documentation for the module in question for particulars. | Item | Description | |--------|--------------| | `desc` | A friendly description that can be found in places such as "Who's Online" or wherever the `%MD` MCI code is used. | | `art` | An art file *spec*. See [General Art Information](../art/general.md). | -| `next` | Specifies the next menu entry to go to next. Can be explicit or an array of possibilities dependent on ACS. See **Flow Control** in the **ACS Checks** section below. If `next` is not supplied, the next menu is this menus parent. Note that special built in methods such as `@systemMethod:logoff` can also be utilized here. | +| `next` | Specifies the menu to go to next. Can be explicit or an array of possibilities dependent on ACS. See **Flow Control** in the **ACS Checks** section below. If `next` is not supplied, the next menu is this menus parent. Note that special built in methods such as `@systemMethod:logoff` can also be utilized here. | | `prompt` | Specifies a prompt, by name, to use along with this menu. Prompts are configured in the `prompts` section. See **Prompts** for more information. | | `submit` | Defines a submit handler when using `prompt`. | `form` | An object defining one or more *forms* available on this menu. | -| `module` | Sets the module name to use for this menu. See **Menu Modules** below. | +| `module` | Sets the module name to use for this menu. The system ships with many build in modules or you can build your own! | | `config` | An object containing additional configuration. See **Config Block** below. | -### Menu Modules -A given menu entry is backed by a *menu module*. That is, the code behind it. Menus are considered "standard" if the `module` member is not specified (and therefore backed by `core/standard_menu.js`). - -See [Menu Modules](../modding/menu-modules.md) for more information. - ### Config Block The `config` block for a menu entry can contain common members as well as a per-module (when `module` is used) settings. @@ -267,6 +270,31 @@ someMenu: { } ``` +## Case Study: Adding a Sub Menu to Main +A very common task: You want to add a new menu accessible from "Main". First, let's create a new menu called "Snazzy Town"! Perhaps under the `mainMenu` entry somewhere, create a new menu: + +```hjson +snazzyTown: { + desc: Snazzy Town + art: snazzy + config: { + cls: true + pause: true + } +} +``` + +Now let's make it accessible by "S" from the main menu. By default the main menu entry is named `mainMenu`. Within the `mainMenu`'s `submit` block you will see some existing action matches to "command". Simply add a new one pointing to `snazzyTown`: + +```hjson +{ + value: { command: "S" } + action: @menu:snazzyTown +} +``` + +That's it! When users type "S" at the main menu, they'll be sent to the Snazzy Town menu. Since we did not supply additional flow logic when they exit, they will fall back to main. + ## Case Study: Adding a New User Password (NUP) You've got a super 31337 board and want to prevent lamerz! Let's run through adding a NUP to your application flow. From 788f1346e3d3aad971399f2b2f86f276d3db2827 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 11 Dec 2020 10:08:17 +0000 Subject: [PATCH 018/146] Bump ini from 1.3.5 to 1.3.7 Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.7. - [Release notes](https://github.com/isaacs/ini/releases) - [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.7) Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index ba4ae5b7..73393eb1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1161,9 +1161,9 @@ ini-config-parser@^1.0.4: rimraf "^2.6.1" ini@~1.3.0: - version "1.3.5" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" - integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== + version "1.3.7" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84" + integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ== inquirer@7.3.3: version "7.3.3" From 878e73e5d7758d08a1c8e60b6d28c4a02aaac088 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 11 Dec 2020 14:58:00 -0700 Subject: [PATCH 019/146] Add method for optimizing DBs --- core/system_menu_method.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) 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 From ceeda8b13f12fb25b2adb6e72518b4280c2ea9c7 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 12 Dec 2020 12:35:01 -0700 Subject: [PATCH 020/146] Allow default hash tags to be supplied in file areas * Supply array or comma separated list of strings via 'hashTags' property * oputil will use these unless --tags are supplied * Uploads will default to these tags (but user can override) --- WHATSNEW.md | 1 + core/file_base_area.js | 166 ++++++++++++++++--------------- core/oputil/oputil_file_base.js | 5 +- core/upload.js | 1 + docs/filebase/first-file-area.md | 2 + 5 files changed, 95 insertions(+), 80 deletions(-) diff --git a/WHATSNEW.md b/WHATSNEW.md index 057ff329..5030f571 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -15,6 +15,7 @@ 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`. ## 0.0.11-beta * Upgraded from `alpha` to `beta` -- The software is far along and mature enough at this point! 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/oputil/oputil_file_base.js b/core/oputil/oputil_file_base.js index 308d3581..1642b8ac 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) { 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/docs/filebase/first-file-area.md b/docs/filebase/first-file-area.md index ae9e0fd2..db1efed8 100644 --- a/docs/filebase/first-file-area.md +++ b/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" ] } } ``` From 9e7536754ddf913f1b3027263ae0fc5620c2e43d Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 12 Dec 2020 22:05:14 -0700 Subject: [PATCH 021/146] Add ability to pass env to abracadabra --- WHATSNEW.md | 1 + core/abracadabra.js | 1 + docs/modding/local-doors.md | 1 + 3 files changed, 3 insertions(+) diff --git a/WHATSNEW.md b/WHATSNEW.md index 5030f571..c3910cd4 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -16,6 +16,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * 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]). ## 0.0.11-beta * Upgraded from `alpha` to `beta` -- The software is far along and mature enough at this point! diff --git a/core/abracadabra.js b/core/abracadabra.js index b2e641e1..3f21fa0b 100644 --- a/core/abracadabra.js +++ b/core/abracadabra.js @@ -173,6 +173,7 @@ exports.getModule = class AbracadabraModule extends MenuModule { dropFile : this.dropFile.fileName, dropFilePath : this.dropFile.fullPath, node : this.client.node, + env : this.config.env, }; const doorTracking = trackDoorRunBegin(this.client, this.config.name); diff --git a/docs/modding/local-doors.md b/docs/modding/local-doors.md index 3e3e59ad..ccd9a495 100644 --- a/docs/modding/local-doors.md +++ b/docs/modding/local-doors.md @@ -18,6 +18,7 @@ The `abracadabra` `config` block can contain the following members: | `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`. | From 12ca811476f36a704d52f367c072269c6a2383ce Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 12 Dec 2020 23:03:33 -0700 Subject: [PATCH 022/146] Fix multi-match file area wildcard --- core/oputil/oputil_file_base.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/oputil/oputil_file_base.js b/core/oputil/oputil_file_base.js index 1642b8ac..5531695d 100644 --- a/core/oputil/oputil_file_base.js +++ b/core/oputil/oputil_file_base.js @@ -552,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); From f202600571ed8732d3f615077ba735831484b9cd Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 14 Dec 2020 12:02:25 -0700 Subject: [PATCH 023/146] dropFileType is now optional when using abracadabra * Can be left out or set to 'none' * Allows doors that do not use a drop file * Additionally: Clean up drop file upon door exit if one is used --- WHATSNEW.md | 1 + core/abracadabra.js | 38 +++++++++++++++++++++++-------------- docs/modding/local-doors.md | 2 +- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/WHATSNEW.md b/WHATSNEW.md index c3910cd4..79ec152d 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -17,6 +17,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * 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`. ## 0.0.11-beta * Upgraded from `alpha` to `beta` -- The software is far along and mature enough at this point! diff --git a/core/abracadabra.js b/core/abracadabra.js index 3f21fa0b..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,18 +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/docs/modding/local-doors.md b/docs/modding/local-doors.md index ccd9a495..c52fc522 100644 --- a/docs/modding/local-doors.md +++ b/docs/modding/local-doors.md @@ -14,7 +14,7 @@ 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`. | From 94d4a52530c1733ffc67d15153230cc297a3415b Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 26 Dec 2020 15:08:37 -0700 Subject: [PATCH 024/146] Add support for stylizing quote indicators ( XX>) --- WHATSNEW.md | 1 + art/themes/luciano_blocktronics/theme.hjson | 7 +++++ core/fse.js | 35 +++++++++++++++++++-- core/message.js | 8 ++--- core/multi_line_edit_text_view.js | 5 ++- 5 files changed, 46 insertions(+), 10 deletions(-) diff --git a/WHATSNEW.md b/WHATSNEW.md index 79ec152d..b457806d 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -18,6 +18,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * 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 and the next for the '>' character). 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. ## 0.0.11-beta * Upgraded from `alpha` to `beta` -- The software is far along and mature enough at this point! diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 407e8a1b..a6b6e6b3 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -538,6 +538,13 @@ // The 'msg_list' module looks for this entry by default messageAreaViewPost: { + config: { + quoteStyleLevel1: [ + "|00|11", + "|00|08", + ] + } + 0: { mci: { TL1: { diff --git a/core/fse.js b/core/fse.js index ba278a01..3237a713 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, + pipeToAnsi, +} = 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,33 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } ); } else { - bodyMessageView.setText(stripAnsiControlCodes(msg)); + msg = stripAnsiControlCodes(msg); // start clean + + // + // In *View* mode, if enabled, do a little prep work so we can stylize: + // - Quote indicators + // - Tear lines + // - Origins + // + if (this.menuConfig.config.quoteStyleLevel1) { + let quoteStyleLevel1 = this.menuConfig.config.quoteStyleLevel1; + // can be a single style to cover XX> or an array to cover XX and > + if (!Array.isArray(quoteStyleLevel1)) { + quoteStyleLevel1 = [ quoteStyleLevel1 ]; + } + if (quoteStyleLevel1.length < 2) { + quoteStyleLevel1.push(quoteStyleLevel1); + } + + const QuoteRegex = /^ ([A-Za-z0-9]{1,2})>([ ]+)/gm; + msg = msg.replace(QuoteRegex, (m, initials, spc) => { + return pipeToAnsi( + ` ${quoteStyleLevel1[0]}${initials}${quoteStyleLevel1[1]}>${bodyMessageView.styleSGR1}${spc}` + ); + }); + } + + bodyMessageView.setText(msg); } } } 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/multi_line_edit_text_view.js b/core/multi_line_edit_text_view.js index 9f099b16..99100c71 100644 --- a/core/multi_line_edit_text_view.js +++ b/core/multi_line_edit_text_view.js @@ -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; From 4533dc3e010577778bfc58f31fe77d35fda7aa66 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 27 Dec 2020 00:08:45 -0700 Subject: [PATCH 025/146] Better quote style support --- WHATSNEW.md | 2 +- art/themes/luciano_blocktronics/theme.hjson | 1 + core/fse.js | 18 +++++++++--------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/WHATSNEW.md b/WHATSNEW.md index b457806d..99629735 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -18,7 +18,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * 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 and the next for the '>' character). 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 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. ## 0.0.11-beta * Upgraded from `alpha` to `beta` -- The software is far along and mature enough at this point! diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index a6b6e6b3..58db21f4 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -542,6 +542,7 @@ quoteStyleLevel1: [ "|00|11", "|00|08", + "|00|03", ] } diff --git a/core/fse.js b/core/fse.js index 3237a713..83d81359 100644 --- a/core/fse.js +++ b/core/fse.js @@ -444,19 +444,19 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul // - Origins // if (this.menuConfig.config.quoteStyleLevel1) { - let quoteStyleLevel1 = this.menuConfig.config.quoteStyleLevel1; - // can be a single style to cover XX> or an array to cover XX and > - if (!Array.isArray(quoteStyleLevel1)) { - quoteStyleLevel1 = [ quoteStyleLevel1 ]; + let styleL1 = this.menuConfig.config.quoteStyleLevel1; + // can be a single style to cover 'XX> TEXT' or an array to cover 'XX', '>', and TEXT + if (!Array.isArray(styleL1)) { + styleL1 = [ styleL1 ]; } - if (quoteStyleLevel1.length < 2) { - quoteStyleLevel1.push(quoteStyleLevel1); + while (styleL1.length < 3) { + styleL1.push(styleL1); } - const QuoteRegex = /^ ([A-Za-z0-9]{1,2})>([ ]+)/gm; - msg = msg.replace(QuoteRegex, (m, initials, spc) => { + const QuoteRegex = /^ ([A-Za-z0-9]{1,2})>([ ]+)([^\r\n]*\r?\n)/gm; + msg = msg.replace(QuoteRegex, (m, initials, spc, text) => { return pipeToAnsi( - ` ${quoteStyleLevel1[0]}${initials}${quoteStyleLevel1[1]}>${bodyMessageView.styleSGR1}${spc}` + ` ${styleL1[0]}${initials}${styleL1[1]}>${spc}${styleL1[2]}${text}${bodyMessageView.styleSGR1}` ); }); } From d568cb993d1ed3e4073f2ae26981370f7fc90356 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 28 Dec 2020 17:04:11 -0700 Subject: [PATCH 026/146] Add support for tear line and origin line style --- WHATSNEW.md | 1 + art/themes/luciano_blocktronics/theme.hjson | 9 +++++ core/fse.js | 41 +++++++++++++++++---- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/WHATSNEW.md b/WHATSNEW.md index 99629735..ccef1dda 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -19,6 +19,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For * 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! diff --git a/art/themes/luciano_blocktronics/theme.hjson b/art/themes/luciano_blocktronics/theme.hjson index 58db21f4..2ce388d0 100644 --- a/art/themes/luciano_blocktronics/theme.hjson +++ b/art/themes/luciano_blocktronics/theme.hjson @@ -544,6 +544,15 @@ "|00|08", "|00|03", ] + tearLineStyle: [ + "|00|08", + "|00|02", + ] + originStyle: [ + "|00|08", + "|00|06", + "|00|03", + ] } 0: { diff --git a/core/fse.js b/core/fse.js index 83d81359..f5778211 100644 --- a/core/fse.js +++ b/core/fse.js @@ -437,6 +437,16 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul } else { msg = stripAnsiControlCodes(msg); // start clean + const styleToArray = (style, len) => { + if (!Array.isArray(style)) { + style = [ style ]; + } + while (style.length < len) { + style.push(style); + } + return style; + }; + // // In *View* mode, if enabled, do a little prep work so we can stylize: // - Quote indicators @@ -444,14 +454,8 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul // - Origins // if (this.menuConfig.config.quoteStyleLevel1) { - let styleL1 = this.menuConfig.config.quoteStyleLevel1; // can be a single style to cover 'XX> TEXT' or an array to cover 'XX', '>', and TEXT - if (!Array.isArray(styleL1)) { - styleL1 = [ styleL1 ]; - } - while (styleL1.length < 3) { - styleL1.push(styleL1); - } + const styleL1 = styleToArray(this.menuConfig.config.quoteStyleLevel1, 3); const QuoteRegex = /^ ([A-Za-z0-9]{1,2})>([ ]+)([^\r\n]*\r?\n)/gm; msg = msg.replace(QuoteRegex, (m, initials, spc, text) => { @@ -461,6 +465,29 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul }); } + 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 pipeToAnsi( + `${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 pipeToAnsi( + `${spc}${style[0]}* ${style[1]}Origin: ${style[2]}${text}${bodyMessageView.styleSGR1}` + ); + }); + } + bodyMessageView.setText(msg); } } From 6abd166d22b86e8428768b11ee37f919b8112145 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 31 Dec 2020 01:20:46 -0700 Subject: [PATCH 027/146] Much improved --- core/fse.js | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/core/fse.js b/core/fse.js index f5778211..2096f293 100644 --- a/core/fse.js +++ b/core/fse.js @@ -23,7 +23,7 @@ const { } = require('./string_util.js'); const { stripMciColorCodes, - pipeToAnsi, + controlCodesToAnsi, } = require('./color_codes.js'); const Config = require('./config.js').get; const { getAddressedToInfo } = require('./mail_util.js'); @@ -459,9 +459,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul const QuoteRegex = /^ ([A-Za-z0-9]{1,2})>([ ]+)([^\r\n]*\r?\n)/gm; msg = msg.replace(QuoteRegex, (m, initials, spc, text) => { - return pipeToAnsi( - ` ${styleL1[0]}${initials}${styleL1[1]}>${spc}${styleL1[2]}${text}${bodyMessageView.styleSGR1}` - ); + return ` ${styleL1[0]}${initials}${styleL1[1]}>${spc}${styleL1[2]}${text}${bodyMessageView.styleSGR1}`; }); } @@ -471,9 +469,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul const TearLineRegex = /^--- (.+)$(?![\s\S]*^--- .+$)/m; msg = msg.replace(TearLineRegex, (m, text) => { - return pipeToAnsi( - `${style[0]}--- ${style[1]}${text}${bodyMessageView.styleSGR1}` - ); + return `${style[0]}--- ${style[1]}${text}${bodyMessageView.styleSGR1}`; }); } @@ -482,13 +478,11 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul const OriginRegex = /^([ ]{1,2})\* Origin: (.+)$/m; msg = msg.replace(OriginRegex, (m, spc, text) => { - return pipeToAnsi( - `${spc}${style[0]}* ${style[1]}Origin: ${style[2]}${text}${bodyMessageView.styleSGR1}` - ); + return `${spc}${style[0]}* ${style[1]}Origin: ${style[2]}${text}${bodyMessageView.styleSGR1}`; }); } - bodyMessageView.setText(msg); + bodyMessageView.setText(controlCodesToAnsi(msg)); } } } From 14b1b2b14580ee7c6f6c3799d6845b9564892cf3 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 31 Dec 2020 15:35:44 -0700 Subject: [PATCH 028/146] Yet more FSE color/style improvements --- core/color_codes.js | 9 +++++---- core/fse.js | 7 ++++--- 2 files changed, 9 insertions(+), 7 deletions(-) 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/fse.js b/core/fse.js index 2096f293..c085ef2c 100644 --- a/core/fse.js +++ b/core/fse.js @@ -455,11 +455,12 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul // 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 = /^ ([A-Za-z0-9]{1,2})>([ ]+)([^\r\n]*\r?\n)/gm; - msg = msg.replace(QuoteRegex, (m, initials, spc, text) => { - return ` ${styleL1[0]}${initials}${styleL1[1]}>${spc}${styleL1[2]}${text}${bodyMessageView.styleSGR1}`; + 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}`; }); } From 862031c57f3077ea041c118f0e9b451a2dd01712 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 2 Jan 2021 15:04:16 -0700 Subject: [PATCH 029/146] Bug fix: Word wrap when dealing with pipe codes Bug fix: Assumed FES styleizers --- core/fse.js | 2 +- core/string_util.js | 39 +++++++++++++++++++++++++++++++++++++-- core/word_wrap.js | 10 ++++++---- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/core/fse.js b/core/fse.js index c085ef2c..9fa0e97b 100644 --- a/core/fse.js +++ b/core/fse.js @@ -442,7 +442,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul style = [ style ]; } while (style.length < len) { - style.push(style); + style.push(style[0]); } return style; }; diff --git a/core/string_util.js b/core/string_util.js index 6887275e..407c06c5 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/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. From 1b65dccb48dc3f8b895d0b0a6b647c75aaa69bb2 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 11 Jan 2021 19:56:54 -0700 Subject: [PATCH 030/146] Fix VTX link, hopefully --- docs/servers/websocket.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/servers/websocket.md b/docs/servers/websocket.md index 1bfa0583..98bf5b76 100644 --- a/docs/servers/websocket.md +++ b/docs/servers/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](../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: From a8aa746c7db98cd65451dad55680cdf143f876e5 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 11 Jan 2021 19:58:59 -0700 Subject: [PATCH 031/146] Makes sense... --- docs/servers/websocket.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/servers/websocket.md b/docs/servers/websocket.md index 98bf5b76..a2c26754 100644 --- a/docs/servers/websocket.md +++ b/docs/servers/websocket.md @@ -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: From 9d837cde4d5a548a4c2e8457980225eb039d1410 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 2 Feb 2021 18:58:39 -0700 Subject: [PATCH 032/146] Add the latest NR term type -- kludge until ansi-bbs-utils is put in --- core/client_term.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/client_term.js b/core/client_term.js index 4cbd603c..d390d8ac 100644 --- a/core/client_term.js +++ b/core/client_term.js @@ -153,7 +153,7 @@ 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); + return [ 'ansi', 'pcansi', 'pc-ansi', 'ansi-bbs', 'qansi', 'scoansi', 'syncterm', 'ansi-256color', 'ansi-256color-rgb' ].includes(this.termType); }; // :TODO: probably need to update these to convert IAC (0xff) -> IACIAC (escape it) From 31f94efdd8454cfb12e689387973e47d9e1690f9 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 4 Feb 2021 19:27:57 -0700 Subject: [PATCH 033/146] Better handling of bots hitting bad Gopher selectors --- core/servers/content/gopher.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/core/servers/content/gopher.js b/core/servers/content/gopher.js index 442ecdcb..95c0eef1 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(/[^ -~]/g, ''); + if (data) { + this.routeRequest(data, socket); + } else { + this.notFoundGenerator('**invalid selector**', res => { + return socket.end(`${res}`); + }); + } }); socket.on('error', err => { From 392dc160a09d469e62e63e268656c7f45a9388f4 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 4 Feb 2021 19:32:28 -0700 Subject: [PATCH 034/146] Fix a dumb --- core/servers/content/gopher.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/servers/content/gopher.js b/core/servers/content/gopher.js index 95c0eef1..a817bd22 100644 --- a/core/servers/content/gopher.js +++ b/core/servers/content/gopher.js @@ -89,7 +89,7 @@ exports.getModule = class GopherModule extends ServerModule { socket.on('data', data => { // sanitize a bit - bots like to inject garbage - data = data.replace(/[^ -~]/g, ''); + data = data.replace(/[^ -~\t\r\n]/g, ''); if (data) { this.routeRequest(data, socket); } else { From 48e42d2ffd0c86114a9cf55d4f3018ae9fe9aa71 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 8 Feb 2021 19:31:29 -0700 Subject: [PATCH 035/146] Update copyright, note on Node.js 14 --- LICENSE.TXT | 2 +- README.md | 2 +- WHATSNEW.md | 1 + core/connect.js | 2 +- core/qwk_mail_packet.js | 2 +- docs/installation/testing.md | 2 +- 6 files changed, 6 insertions(+), 5 deletions(-) diff --git a/LICENSE.TXT b/LICENSE.TXT index af51c707..6dac9ac8 100644 --- a/LICENSE.TXT +++ b/LICENSE.TXT @@ -1,4 +1,4 @@ -Copyright (c) 2015-2020, Bryan D. Ashby +Copyright (c) 2015-2021, 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..990f3d4f 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ ENiGMA has been tested with many terminals. However, the following are suggested ## 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-2021, Bryan D. Ashby All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/WHATSNEW.md b/WHATSNEW.md index ccef1dda..89ee5a7b 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -3,6 +3,7 @@ This document attempts to track **major** changes and additions in ENiGMA½. For ## 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. diff --git a/core/connect.js b/core/connect.js index 64c4ea3e..29d5443d 100644 --- a/core/connect.js +++ b/core/connect.js @@ -199,7 +199,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-2021 Bryan Ashby |14- |12http://l33t.codes/ |06Updates & source |14- |12https://github.com/NuSkooler/enigma-bbs/ |00` ); diff --git a/core/qwk_mail_packet.js b/core/qwk_mail_packet.js index eab57eb7..17a24e0e 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-2021 Bryan Ashby`; this.messagesStream.write(id.padEnd(QWKMessageBlockSize, ' '), 'ascii'); this.currentMessageOffset = QWKMessageBlockSize; diff --git a/docs/installation/testing.md b/docs/installation/testing.md index 2e5dbea1..dde90900 100644 --- a/docs/installation/testing.md +++ b/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-2021, Bryan Ashby _____________________ _____ ____________________ __________\_ / \__ ____/\_ ____ \ /____/ / _____ __ \ / ______/ // /___jp! // __|___// | \// |// | \// | | \// \ /___ /_____ From 57d8bdfa962e3073d4a531e074d9cadb43aa298e Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 8 Feb 2021 19:33:41 -0700 Subject: [PATCH 036/146] Remove old board --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 990f3d4f..6b53d0d7 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,6 @@ 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 * [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. From ee653d7211ed0992298b1def6f84d5ac287386c7 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 8 Feb 2021 19:47:46 -0700 Subject: [PATCH 037/146] Various dependency updates * Should resolve Left over enigftnexport files in enigma/tmp folder #322 --- package.json | 24 ++--- yarn.lock | 299 ++++++++++++++++++++++++++++----------------------- 2 files changed, 178 insertions(+), 145 deletions(-) diff --git a/package.json b/package.json index 5f7b0b34..555c01cf 100644 --- a/package.json +++ b/package.json @@ -22,29 +22,29 @@ "retro" ], "dependencies": { + "@breejs/later": "^4.0.2", "async": "3.2.0", "binary-parser": "1.7.0", "buffers": "github:NuSkooler/node-buffers", - "bunyan": "1.8.14", - "deepdash": "5.3.2", + "bunyan": "1.8.15", + "deepdash": "5.3.5", "exiftool": "^0.0.3", - "fs-extra": "9.0.1", + "fs-extra": "9.1.0", "glob": "7.1.6", - "graceful-fs": "^4.2.4", - "hashids": "2.2.2", + "graceful-fs": "4.2.5", + "hashids": "2.2.8", "hjson": "3.2.2", "iconv-lite": "^0.6.2", "ini-config-parser": "^1.0.4", "inquirer": "7.3.3", - "@breejs/later" : "^4.0.2", "lodash": "4.17.20", "lru-cache": "^5.1.1", - "mime-types": "2.1.27", + "mime-types": "2.1.28", "minimist": "1.2.5", "moment": "2.29.1", "nntp-server": "^1.0.3", - "node-pty": "^0.9.0", - "nodemailer": "6.4.16", + "node-pty": "0.10.0", + "nodemailer": "6.4.17", "otplib": "11.0.1", "qrcode-generator": "^1.4.4", "rlogin": "^1.0.0", @@ -55,14 +55,14 @@ "ssh2": "0.8.9", "telnet-socket": "^0.2.3", "temptmp": "^1.1.0", - "uuid": "8.3.1", + "uuid": "8.3.2", "uuid-parse": "1.1.0", - "ws": "7.4.0", + "ws": "7.4.3", "xxhash": "^0.3.0", "yazl": "^2.5.1" }, "devDependencies": { - "eslint": "7.13.0" + "eslint": "7.19.0" }, "engines": { "node": ">=12" diff --git a/yarn.lock b/yarn.lock index 73393eb1..eb799863 100644 --- a/yarn.lock +++ b/yarn.lock @@ -36,10 +36,10 @@ exec-sh "^0.3.2" minimist "^1.2.0" -"@eslint/eslintrc@^0.2.1": - version "0.2.1" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.2.1.tgz#f72069c330461a06684d119384435e12a5d76e3c" - integrity sha512-XRUeBZ5zBWLYgSANMpThFddrZZkEbGHgUdt5UJjZfnlN9BGCiUBrf+nvbRupSjMvqzwnQN0qwCmOxITt1cfywA== +"@eslint/eslintrc@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.3.0.tgz#d736d6963d7003b6514e6324bec9c602ac340318" + integrity sha512-1JTKgrOKAHVivSvOYw+sJOunkBjUOvjqWk1DPja7ZFhIS2mX/4EgTT8M7eTK9jrKhL/FvXXEbQwIs3pg1xp3dg== dependencies: ajv "^6.12.4" debug "^4.1.1" @@ -48,7 +48,7 @@ ignore "^4.0.6" import-fresh "^3.2.1" js-yaml "^3.13.1" - lodash "^4.17.19" + lodash "^4.17.20" minimatch "^3.0.4" strip-json-comments "^3.1.1" @@ -67,12 +67,17 @@ acorn-jsx@^5.2.0: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.2.0.tgz#4c66069173d6fdd68ed85239fc256226182b2ebe" integrity sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ== +acorn-jsx@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" + integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== + acorn@^7.4.0: version "7.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -ajv@^6.10.0, ajv@^6.10.2: +ajv@^6.10.0: version "6.12.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd" integrity sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ== @@ -92,6 +97,16 @@ ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-7.0.4.tgz#827e5f5ae32f5e5c1637db61f253a112229b5e2f" + integrity sha512-xzzzaqgEQfmuhbhAoqjJ8T/1okb6gAzXn/eQRNpAN1AEUoHJTNF9xCDRTtf/s3SKldtZfa+RJeTs+BQq+eZ/sw== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + ansi-colors@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" @@ -124,13 +139,20 @@ ansi-regex@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== -ansi-styles@^3.2.0, ansi-styles@^3.2.1: +ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== dependencies: color-convert "^1.9.0" +ansi-styles@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + ansi-styles@^4.1.0: version "4.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" @@ -211,10 +233,10 @@ assign-symbols@^1.0.0: resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= -astral-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" - integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== async@3.2.0: version "3.2.0" @@ -306,10 +328,10 @@ buffer-crc32@~0.2.3: version "0.1.1" resolved "https://codeload.github.com/NuSkooler/node-buffers/tar.gz/cd0855598f7048b02f0a51c90e22573973e9e2c2" -bunyan@1.8.14: - version "1.8.14" - resolved "https://registry.yarnpkg.com/bunyan/-/bunyan-1.8.14.tgz#3d8c1afea7de158a5238c7cb8a66ab6b38dd45b4" - integrity sha512-LlahJUxXzZLuw/hetUQJmRgZ1LF6+cr5TPpRj6jf327AsiIq2jhYEH4oqUUkVKTor+9w2BT3oxVwhzE5lw9tcg== +bunyan@1.8.15: + version "1.8.15" + resolved "https://registry.yarnpkg.com/bunyan/-/bunyan-1.8.15.tgz#8ce34ca908a17d0776576ca1b2f6cbd916e93b46" + integrity sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig== optionalDependencies: dtrace-provider "~0.8" moment "^2.19.3" @@ -527,13 +549,13 @@ deep-is@^0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= -deepdash@5.3.2: - version "5.3.2" - resolved "https://registry.yarnpkg.com/deepdash/-/deepdash-5.3.2.tgz#7ae24fbfa94420b6365ec3384a54d52327481019" - integrity sha512-Qm2IUr6GsPJQJNATj2g93VvC6n5raH+WtBQw78LNkgxYmlFLV94/l1NcqlsmuPhJV5+GndIsWcPJ6E69kiFIAg== +deepdash@5.3.5: + version "5.3.5" + resolved "https://registry.yarnpkg.com/deepdash/-/deepdash-5.3.5.tgz#611bec9c1f2829832d21971dcbefe712e408647d" + integrity sha512-1ZdPPCI1pCEqeAWGSw+Nbpb/2iIV4w3sGPc22H/PDtcApb8+psTzPIoOVD040iBaT2wqZab29Kjrz6G+cd5mqQ== dependencies: lodash "^4.17.20" - lodash-es "^4.17.15" + lodash-es "^4.17.20" define-property@^0.2.5: version "0.2.5" @@ -603,11 +625,6 @@ dtrace-provider@~0.8: dependencies: nan "^2.10.0" -emoji-regex@^7.0.1: - version "7.0.3" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" - integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== - emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -662,13 +679,13 @@ eslint-visitor-keys@^2.0.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== -eslint@7.13.0: - version "7.13.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.13.0.tgz#7f180126c0dcdef327bfb54b211d7802decc08da" - integrity sha512-uCORMuOO8tUzJmsdRtrvcGq5qposf7Rw0LwkTJkoDbOycVQtQjmnhZSuLQnozLE4TmAzlMVV45eCHmQ1OpDKUQ== +eslint@7.19.0: + version "7.19.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.19.0.tgz#6719621b196b5fad72e43387981314e5d0dc3f41" + integrity sha512-CGlMgJY56JZ9ZSYhJuhow61lMPPjUzWmChFya71Z/jilVos7mR/jPgaEfVGgMBY5DshbKdG8Ezb8FDCHcoMEMg== dependencies: "@babel/code-frame" "^7.0.0" - "@eslint/eslintrc" "^0.2.1" + "@eslint/eslintrc" "^0.3.0" ajv "^6.10.0" chalk "^4.0.0" cross-spawn "^7.0.2" @@ -678,10 +695,10 @@ eslint@7.13.0: eslint-scope "^5.1.1" eslint-utils "^2.1.0" eslint-visitor-keys "^2.0.0" - espree "^7.3.0" + espree "^7.3.1" esquery "^1.2.0" esutils "^2.0.2" - file-entry-cache "^5.0.1" + file-entry-cache "^6.0.0" functional-red-black-tree "^1.0.1" glob-parent "^5.0.0" globals "^12.1.0" @@ -692,7 +709,7 @@ eslint@7.13.0: js-yaml "^3.13.1" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" - lodash "^4.17.19" + lodash "^4.17.20" minimatch "^3.0.4" natural-compare "^1.4.0" optionator "^0.9.1" @@ -701,7 +718,7 @@ eslint@7.13.0: semver "^7.2.1" strip-ansi "^6.0.0" strip-json-comments "^3.1.0" - table "^5.2.3" + table "^6.0.4" text-table "^0.2.0" v8-compile-cache "^2.0.3" @@ -714,6 +731,15 @@ espree@^7.3.0: acorn-jsx "^5.2.0" eslint-visitor-keys "^1.3.0" +espree@^7.3.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" + integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g== + dependencies: + acorn "^7.4.0" + acorn-jsx "^5.3.1" + eslint-visitor-keys "^1.3.0" + esprima@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" @@ -856,12 +882,12 @@ figures@^3.0.0: dependencies: escape-string-regexp "^1.0.5" -file-entry-cache@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c" - integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g== +file-entry-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.0.tgz#7921a89c391c6d93efec2169ac6bf300c527ea0a" + integrity sha512-fqoO76jZ3ZnYrXLDRxBR1YvOvc0k844kcOg40bgsPrE25LAb/PDqTY+ho64Xh2c8ZXgIKldchCFHczG2UVRcWA== dependencies: - flat-cache "^2.0.1" + flat-cache "^3.0.4" fill-range@^4.0.0: version "4.0.0" @@ -873,19 +899,18 @@ fill-range@^4.0.0: repeat-string "^1.6.1" to-regex-range "^2.1.0" -flat-cache@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" - integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA== +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== dependencies: - flatted "^2.0.0" - rimraf "2.6.3" - write "1.0.3" + flatted "^3.1.0" + rimraf "^3.0.2" -flatted@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" - integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== +flatted@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469" + integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA== for-in@^1.0.2: version "1.0.2" @@ -907,15 +932,15 @@ from2@^2.3.0: inherits "^2.0.1" readable-stream "^2.0.0" -fs-extra@9.0.1: - version "9.0.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc" - integrity sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ== +fs-extra@9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== dependencies: at-least-node "^1.0.0" graceful-fs "^4.2.0" jsonfile "^6.0.1" - universalify "^1.0.0" + universalify "^2.0.0" fs-minipass@^1.2.5: version "1.2.5" @@ -1020,6 +1045,11 @@ globby@^6.1.0: pify "^2.0.0" pinkie-promise "^2.0.0" +graceful-fs@4.2.5: + version "4.2.5" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.5.tgz#bc18864a6c9fc7b303f2e2abdb9155ad178fbe29" + integrity sha512-kBBSQbz2K0Nyn+31j/w36fUfxkBW9/gfwRWdUY1ULReH3iokVJgddZAFcD1D0xlgTmFxJCbUkUclAlc6/IDJkw== + graceful-fs@^4.1.6: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" @@ -1030,11 +1060,6 @@ graceful-fs@^4.2.0: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.0.tgz#8d8fdc73977cb04104721cb53666c1ca64cd328b" integrity sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg== -graceful-fs@^4.2.4: - version "4.2.4" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" - integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== - has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -1081,10 +1106,10 @@ has-values@^1.0.0: is-number "^3.0.0" kind-of "^4.0.0" -hashids@2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/hashids/-/hashids-2.2.2.tgz#5381257b34f0d0aca2063967b14a30a9312f650d" - integrity sha512-nqH06JLFeBBCg31jYSArbOs/XddTdnHSul/hb6XbrWg/UlUWFQc9KR21sVLoGKQGKPW1D3zgTgTew0oY7nhrhQ== +hashids@2.2.8: + version "2.2.8" + resolved "https://registry.yarnpkg.com/hashids/-/hashids-2.2.8.tgz#d1e4e1be48e711ad6e5f27b32c37c399d5eeb5b4" + integrity sha512-OI6rsk/OqyX60nBsqxnNl3R7CDv9eMOgxzipshZwXE6cUcBcEyHf8rNDlWVdQJZK3TEynILYybCsNUEQfJsdLA== hjson@3.2.2: version "3.2.2" @@ -1359,6 +1384,11 @@ json-schema-traverse@^0.4.1: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" @@ -1405,17 +1435,17 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -lodash-es@^4.17.15: - version "4.17.15" - resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78" - integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ== +lodash-es@^4.17.20: + version "4.17.20" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.20.tgz#29f6332eefc60e849f869c264bc71126ad61e8f7" + integrity sha512-JD1COMZsq8maT6mnuz1UMV0jvYD0E0aUsSOdrr1/nAG3dhqQXwRRgeW0cSqH1U43INKcqxaiVIQNOUDld7gRDA== lodash@4.17.20, lodash@^4.17.19, lodash@^4.17.20: version "4.17.20" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== -lodash@^4.17.14, lodash@^4.17.15: +lodash@^4.17.15: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== @@ -1465,17 +1495,17 @@ micromatch@^3.1.4: snapdragon "^0.8.1" to-regex "^3.0.2" -mime-db@1.44.0: - version "1.44.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" - integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== +mime-db@1.45.0: + version "1.45.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea" + integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w== -mime-types@2.1.27: - version "2.1.27" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" - integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== +mime-types@2.1.28: + version "2.1.28" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.28.tgz#1160c4757eab2c5363888e005273ecf79d2a0ecd" + integrity sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ== dependencies: - mime-db "1.44.0" + mime-db "1.45.0" mimic-fn@^2.1.0: version "2.1.0" @@ -1656,17 +1686,17 @@ node-pre-gyp@^0.11.0: semver "^5.3.0" tar "^4" -node-pty@^0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.9.0.tgz#8f9bcc0d1c5b970a3184ffd533d862c7eb6590a6" - integrity sha512-MBnCQl83FTYOu7B4xWw10AW77AAh7ThCE1VXEv+JeWj8mSpGo+0bwgsV+b23ljBFwEM9OmsOv3kM27iUPPm84g== +node-pty@0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.10.0.tgz#c98d23967b076b35c9fb216c542a04d0b5db4821" + integrity sha512-Q65ookKbjhqWUYKmtZ6iPn0nnqNdzpm3YJOBmzwWJde/TrenBxK9FgqGGtSW0Wjz4YsR1grQF4a7RS5nBwuW9A== dependencies: nan "^2.14.0" -nodemailer@6.4.16: - version "6.4.16" - resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.4.16.tgz#5cb6391b1d79ab7eff32d6f9f48366b5a7117293" - integrity sha512-68K0LgZ6hmZ7PVmwL78gzNdjpj5viqBdFqKrTtr9bZbJYj6BRj5W6WGkxXrEnUl3Co3CBXi3CZBUlpV/foGnOQ== +nodemailer@6.4.17: + version "6.4.17" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.4.17.tgz#8de98618028953b80680775770f937243a7d7877" + integrity sha512-89ps+SBGpo0D4Bi5ZrxcrCiRFaMmkCt+gItMXQGzEtZVR3uAD3QAQIDoxTWnx3ky0Dwwy/dhFrQ+6NNGXpw/qQ== nopt@^4.0.1: version "4.0.1" @@ -1959,6 +1989,11 @@ repeat-string@^1.6.1: resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -1982,13 +2017,6 @@ ret@~0.1.10: resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== -rimraf@2.6.3: - version "2.6.3" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" - integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== - dependencies: - glob "^7.1.3" - rimraf@^2.2.8, rimraf@^2.6.1: version "2.6.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" @@ -1996,6 +2024,13 @@ rimraf@^2.2.8, rimraf@^2.6.1: dependencies: glob "^7.0.5" +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + rimraf@~2.4.0: version "2.4.5" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.4.5.tgz#ee710ce5d93a8fdb856fb5ea8ff0e2d75934b2da" @@ -2148,14 +2183,14 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= -slice-ansi@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" - integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== dependencies: - ansi-styles "^3.2.0" - astral-regex "^1.0.0" - is-fullwidth-code-point "^2.0.0" + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" snapdragon-node@^2.0.1: version "2.1.1" @@ -2288,15 +2323,6 @@ string-width@^1.0.1: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" -string-width@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" - integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== - dependencies: - emoji-regex "^7.0.1" - is-fullwidth-code-point "^2.0.0" - strip-ansi "^5.1.0" - string-width@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.1.0.tgz#ba846d1daa97c3c596155308063e075ed1c99aff" @@ -2306,6 +2332,15 @@ string-width@^4.1.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^5.2.0" +string-width@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" + integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + string_decoder@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d" @@ -2334,7 +2369,7 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" -strip-ansi@^5.1.0, strip-ansi@^5.2.0: +strip-ansi@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== @@ -2382,15 +2417,15 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -table@^5.2.3: - version "5.4.6" - resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" - integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== +table@^6.0.4: + version "6.0.7" + resolved "https://registry.yarnpkg.com/table/-/table-6.0.7.tgz#e45897ffbcc1bcf9e8a87bf420f2c9e5a7a52a34" + integrity sha512-rxZevLGTUzWna/qBLObOe16kB2RTnnbhciwgPbMMlazz1yZGVEgnZK762xyVdVznhqxrfCeBMmMkgOOaPwjH7g== dependencies: - ajv "^6.10.2" - lodash "^4.17.14" - slice-ansi "^2.1.0" - string-width "^3.0.0" + ajv "^7.0.2" + lodash "^4.17.20" + slice-ansi "^4.0.0" + string-width "^4.2.0" tar@^4: version "4.4.6" @@ -2529,6 +2564,11 @@ universalify@^1.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d" integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug== +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + unset-value@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" @@ -2569,10 +2609,10 @@ uuid-parse@1.1.0: resolved "https://registry.yarnpkg.com/uuid-parse/-/uuid-parse-1.1.0.tgz#7061c5a1384ae0e1f943c538094597e1b5f3a65b" integrity sha512-OdmXxA8rDsQ7YpNVbKSJkNzTw2I+S5WsbMDnCtIWSQaosNAcWtFuI/YK1TjzUI6nbkgiqEyh8gWngfcv8Asd9A== -uuid@8.3.1: - version "8.3.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.1.tgz#2ba2e6ca000da60fce5a196954ab241131e05a31" - integrity sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg== +uuid@8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== v8-compile-cache@^2.0.3: version "2.1.1" @@ -2617,17 +2657,10 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -write@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3" - integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig== - dependencies: - mkdirp "^0.5.1" - -ws@7.4.0: - version "7.4.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.0.tgz#a5dd76a24197940d4a8bb9e0e152bb4503764da7" - integrity sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ== +ws@7.4.3: + version "7.4.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.3.tgz#1f9643de34a543b8edb124bdcbc457ae55a6e5cd" + integrity sha512-hr6vCR76GsossIRsr8OLR9acVVm1jyfEWvhbNjtgPOrfvAlKzvyeg/P6r8RuDjRyrcQoPQT7K0DGEPc7Ae6jzA== xtend@~4.0.1: version "4.0.1" From fffbd424d9807d619a774edd33c96a8c0a560be9 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 9 Feb 2021 20:25:54 -0700 Subject: [PATCH 038/146] This has caused more trouble than it's worth - revisit later --- core/theme.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/theme.js b/core/theme.js index f9cf5792..d8340b65 100644 --- a/core/theme.js +++ b/core/theme.js @@ -560,9 +560,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, From 60f2f54a54c5b9ba343b28a316afb37b8c310092 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 9 Feb 2021 20:49:18 -0700 Subject: [PATCH 039/146] Longstanding missing/bad font map --- core/ansi_term.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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', From 1f7545634b5121d06bd9150a35f826761c8054bd Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 13 Feb 2021 11:05:14 -0700 Subject: [PATCH 040/146] Message import stops #329 --- core/config_default.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/config_default.js b/core/config_default.js index 98aeb4af..c5e0b86b 100644 --- a/core/config_default.js +++ b/core/config_default.js @@ -492,7 +492,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 +501,7 @@ module.exports = () => { }, extract : { cmd : '7za', - args : [ 'e', '-o{extractPath}', '{archivePath}', '{fileList}' ], + args : [ 'e', '-y', '-o{extractPath}', '{archivePath}', '{fileList}' ], }, }, From 2542cb919940f29a27ff644f582bcc7d8b198801 Mon Sep 17 00:00:00 2001 From: David Stephens Date: Fri, 19 Feb 2021 19:06:07 +0000 Subject: [PATCH 041/146] Add Docker build and update docs --- docker/Dockerfile | 45 ++++++++++++++++++++++++++++++++++ docker/bin/sexyz | Bin 0 -> 184128 bytes docs/installation/docker.md | 47 ++++++++++++++++++++++++++++-------- 3 files changed, 82 insertions(+), 10 deletions(-) create mode 100644 docker/Dockerfile create mode 100755 docker/bin/sexyz diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000..56b0d160 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,45 @@ +FROM node:lts-buster-slim + +MAINTAINER Dave Stephens + +ENV NVM_DIR /root/.nvm +ENV DEBIAN_FRONTEND noninteractive + +COPY . /enigma-bbs + +# Do some installing! +RUN apt-get update && apt-get install -y \ + git \ + curl \ + build-essential \ + python \ + libssl-dev \ + lrzsz \ + arj \ + lhasa \ + unrar-free \ + p7zip-full \ + && npm install -g pm2 \ + && cd /enigma-bbs && npm install --only=production \ + && apt-get remove build-essential python 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 + +# 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 + +CMD ["pm2-runtime", "main.js"] diff --git a/docker/bin/sexyz b/docker/bin/sexyz new file mode 100755 index 0000000000000000000000000000000000000000..36924822564b48ea4526a1864cc07d6de5a8f2f4 GIT binary patch literal 184128 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 Ss3$gUkxCJ{9fk@ovch%TK# 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 YFH^3; Date: Fri, 19 Feb 2021 19:11:34 +0000 Subject: [PATCH 042/146] Update Dockerfile maintainer label --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 56b0d160..2ceaee30 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,6 +1,6 @@ FROM node:lts-buster-slim -MAINTAINER Dave Stephens +LABEL maintainer="dave@force9.org" ENV NVM_DIR /root/.nvm ENV DEBIAN_FRONTEND noninteractive From 18f241f03edecbd8244aedd41c02486b3187690f Mon Sep 17 00:00:00 2001 From: Dave Stephens Date: Fri, 19 Feb 2021 22:48:28 +0000 Subject: [PATCH 043/146] Add Docker build action --- .github/workflows/docker.yml | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/docker.yml 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 From c30b52c9d87191e86be20661786fc0d7a3f8ce30 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sat, 27 Mar 2021 00:04:07 -0600 Subject: [PATCH 044/146] fix: ensure we have vars before attempting to fetch from them --- core/servers/login/telnet.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/servers/login/telnet.js b/core/servers/login/telnet.js index 12a3ed36..a6daa89e 100644 --- a/core/servers/login/telnet.js +++ b/core/servers/login/telnet.js @@ -109,8 +109,10 @@ class TelnetClient { // 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) { From fdf2e94399c4762cef8f8ffd6fee41605eed87ba Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 21 Apr 2021 15:15:14 -0600 Subject: [PATCH 045/146] Add link to writeup by Robbie Whiting --- docs/modding/local-doors.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/modding/local-doors.md b/docs/modding/local-doors.md index c52fc522..ce948875 100644 --- a/docs/modding/local-doors.md +++ b/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. From 656a88ba53ec9d6fe5b2dc8eb2a2361b9a7a7e65 Mon Sep 17 00:00:00 2001 From: Jon Radon Date: Mon, 3 May 2021 22:38:05 -0400 Subject: [PATCH 046/146] Add missing quote to docker run documentation --- docs/installation/docker.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation/docker.md b/docs/installation/docker.md index 776c0440..85de3aac 100644 --- a/docs/installation/docker.md +++ b/docs/installation/docker.md @@ -14,7 +14,7 @@ for every operating system on the [Docker website](https://docs.docker.com/engin - Run it: ``` - docker run -p 8888:8888 -v "${HOME}/engima-bbs/config:/enigma-bbs/config enigmabbs/enigma-bbs:latest + docker run -p 8888:8888 -v "${HOME}/engima-bbs/config:/enigma-bbs/config" enigmabbs/enigma-bbs:latest ``` :bulb: Configuration will be stored in `${HOME}/engima-bbs/config`. @@ -46,4 +46,4 @@ Customising the Docker image is easy! ``` docker build -f ./docker/Dockerfile . - ``` \ No newline at end of file + ``` From c0bca4ffad6a6e3b75e2e5e05035742488b20739 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 9 May 2021 09:49:03 +0000 Subject: [PATCH 047/146] Bump lodash from 4.17.20 to 4.17.21 Bumps [lodash](https://github.com/lodash/lodash) from 4.17.20 to 4.17.21. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.20...4.17.21) Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 13 ++++--------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 555c01cf..c828edc4 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "iconv-lite": "^0.6.2", "ini-config-parser": "^1.0.4", "inquirer": "7.3.3", - "lodash": "4.17.20", + "lodash": "4.17.21", "lru-cache": "^5.1.1", "mime-types": "2.1.28", "minimist": "1.2.5", diff --git a/yarn.lock b/yarn.lock index eb799863..21fbc262 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1440,15 +1440,10 @@ lodash-es@^4.17.20: resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.20.tgz#29f6332eefc60e849f869c264bc71126ad61e8f7" integrity sha512-JD1COMZsq8maT6mnuz1UMV0jvYD0E0aUsSOdrr1/nAG3dhqQXwRRgeW0cSqH1U43INKcqxaiVIQNOUDld7gRDA== -lodash@4.17.20, lodash@^4.17.19, lodash@^4.17.20: - version "4.17.20" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" - integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== - -lodash@^4.17.15: - version "4.17.15" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" - integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== +lodash@4.17.21, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== lru-cache@^5.1.1: version "5.1.1" From 7723ab507cc207ce058477f148cfd706715a7b33 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Wed, 12 May 2021 10:34:25 -0600 Subject: [PATCH 048/146] Back to the BBS mention --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6b53d0d7..6e1c5bcd 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ 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)! ...and so many others! This project would be nothing without the BBS and artscene communities! From 6b85de641d68a1a9c5f5b28cf09dbf9cee7c8a08 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Sep 2021 05:01:52 +0000 Subject: [PATCH 049/146] Bump tmpl from 1.0.4 to 1.0.5 Bumps [tmpl](https://github.com/daaku/nodejs-tmpl) from 1.0.4 to 1.0.5. - [Release notes](https://github.com/daaku/nodejs-tmpl/releases) - [Commits](https://github.com/daaku/nodejs-tmpl/commits/v1.0.5) --- updated-dependencies: - dependency-name: tmpl dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 21fbc262..d979271b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2481,9 +2481,9 @@ tmp@^0.0.33: os-tmpdir "~1.0.2" tmpl@1.0.x: - version "1.0.4" - resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" - integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE= + version "1.0.5" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" + integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== to-object-path@^0.3.0: version "0.3.0" From e0ecf37869b9b760bd2c658660fe145fc5d65d43 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 22 Oct 2021 10:29:47 -0600 Subject: [PATCH 050/146] FIX: Certain telnet clients make the BBS crashes. #369 userVars -> uservars to match telnet-socket impl. --- core/servers/login/telnet.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/servers/login/telnet.js b/core/servers/login/telnet.js index a6daa89e..b3e13a8b 100644 --- a/core/servers/login/telnet.js +++ b/core/servers/login/telnet.js @@ -103,7 +103,7 @@ 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' ); @@ -111,7 +111,7 @@ class TelnetClient { const getValue = (name) => { return command.optionData.vars && (command.optionData.vars.find(nv => nv.name === name) || - command.optionData.userVars.find(nv => nv.name === name) + command.optionData.uservars.find(nv => nv.name === name) ); }; From 3c71f88cb83db88bcd5d6a0357bb171b1bba749d Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Wed, 12 Jan 2022 10:38:47 -0600 Subject: [PATCH 051/146] Initial support for full menu --- core/full_menu_view.js | 392 +++++++++++++++++++++++++++++++++++++++ core/mci_view_factory.js | 15 +- core/menu_view.js | 12 +- 3 files changed, 417 insertions(+), 2 deletions(-) create mode 100644 core/full_menu_view.js diff --git a/core/full_menu_view.js b/core/full_menu_view.js new file mode 100644 index 00000000..87346b9d --- /dev/null +++ b/core/full_menu_view.js @@ -0,0 +1,392 @@ +/* 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); + + this.initDefaultWidth(); + + const self = this; + + // 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 = function() { + 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); + } + + // Calculate number of items visible after adjusting height + 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; + } + + }; + + this.autoAdjustHeightIfEnabled(); + + this.getSpacer = function() { + return new Array(self.itemHorizSpacing + 1).join(this.fillChar); + } + + this.cachePositions = function() { + if (this.positionCacheExpired) { + this.autoAdjustHeightIfEnabled(); + + var col = self.position.col; + var row = self.position.row; + var spacer = self.getSpacer(); + + var itemInRow = 0; + + for (var i = 0; i < self.items.length; ++i) { + itemInRow++; + self.items[i].row = row; + self.items[i].col = col; + + row += this.itemSpacing + 1; + + // handle going to next column + if (itemInRow == this.itemsPerRow) { + itemInRow = 0; + + row = self.position.row; + var maxLength = 0; + for (var j = 0; j < this.itemsPerRow; j++) { + // TODO: handle complex items + var itemLength = this.items[i - j].text.length; + if (itemLength > maxLength) { + maxLength = itemLength; + } + } + + // set length on each item in the column + for (var j = 0; j < this.itemsPerRow; j++) { + self.items[i - j].fixedLength = maxLength; + } + + // increment the column + col += maxLength + spacer.length + 1; + } + + // also have to calculate the max length on the last column + else if (i == self.items.length - 1) { + var maxLength = 0; + for (var j = 0; j < this.itemsPerRow; j++) { + if (self.items[i - j].col != self.items[i].col) { + break; + } + var itemLength = this.items[i - j].text.length; + if (itemLength > maxLength) { + maxLength = itemLength; + } + } + + // set length on each item in the column + for (var j = 0; j < this.itemsPerRow; j++) { + if (self.items[i - j].col != self.items[i].col) { + break; + } + self.items[i - j].fixedLength = maxLength; + } + + } + } + } + + this.positionCacheExpired = false; + }; + + this.drawItem = function(index) { + const item = self.items[index]; + if (!item) { + return; + } + + const cached = this.getRenderCacheItem(index, item.focused); + if (cached) { + return self.client.term.write(`${ansi.goto(item.row, item.col)}${cached}`); + } + + let text; + let sgr; + if (item.focused && self.hasFocusItems()) { + const focusItem = self.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 === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); + } else { + text = strUtil.stylizeString(item.text, item.focused ? self.focusTextStyle : self.textStyle); + sgr = (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); + } + + text = `${sgr}${strUtil.pad(text, this.dimens.width, this.fillChar, this.justify)}`; + self.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(); + + // :TODO: rename positionCacheExpired to something that makese sense; combine methods for such + if (this.positionCacheExpired) { + this.autoAdjustHeightIfEnabled(); + this.positionCacheExpired = false; + } + + // erase old items + // :TODO: optimize this: only needed if a item is removed or new max width < old. + if (this.oldDimens) { + const blank = new Array(Math.max(this.oldDimens.width, this.dimens.width)).join(' '); + let seq = ansi.goto(this.position.row, this.position.col) + this.getSGR() + blank; + let row = this.position.row + 1; + const endRow = (row + this.oldDimens.height) - 2; + + while (row <= endRow) { + seq += ansi.goto(row, this.position.col) + blank; + row += 1; + } + this.client.term.write(seq); + delete this.oldDimens; + } + + if (this.items.length) { + for (let i = 0; i < this.items.length; ++i) { + this.items[i].focused = this.focusedItemIndex === i; + this.drawItem(i); + } + } +}; + +FullMenuView.prototype.setHeight = function(height) { + FullMenuView.super_.prototype.setHeight.call(this, height); + + this.positionCacheExpired = true; + this.autoAdjustHeight = false; +}; + +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.redraw(); +}; + +FullMenuView.prototype.setFocusItemIndex = function(index) { + FullMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex + + this.redraw(); +}; + +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); +}; + +// :TODO: Apply draw optimizaitons when only two items need drawn vs entire view! + +FullMenuView.prototype.focusNext = function() { + if (this.items.length - 1 === this.focusedItemIndex) { + this.focusedItemIndex = 0; + + } else { + this.focusedItemIndex++; + + } + + this.redraw(); + + FullMenuView.super_.prototype.focusNext.call(this); +}; + +FullMenuView.prototype.focusPrevious = function() { + if (0 === this.focusedItemIndex) { + this.focusedItemIndex = this.items.length - 1; + + + } else { + this.focusedItemIndex--; + } + + this.redraw(); + + FullMenuView.super_.prototype.focusPrevious.call(this); +}; + +FullMenuView.prototype.focusPreviousColumn = function() { + + this.focusedItemIndex = this.focusedItemIndex - this.itemsPerRow; + if (this.focusedItemIndex < 0) { + // add the negative index to the end of the list + this.focusedItemIndex = this.items.length + this.focusedItemIndex; + } + + 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() { + + this.focusedItemIndex = this.focusedItemIndex + this.itemsPerRow; + if (this.focusedItemIndex > this.items.length - 1) { + // add the overflow to the beginning of the list + this.focusedItemIndex = this.focusedItemIndex - this.items.length; + } + + 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() { + // + // Jump to current - up to page size or top + // If already at the top, jump to bottom + // + if (0 === this.focusedItemIndex) { + return this.focusPrevious(); // will jump to bottom + } + + const index = Math.max(this.focusedItemIndex - this.dimens.height, 0); + + this.setFocusItemIndex(index); + + return FullMenuView.super_.prototype.focusPreviousPageItem.call(this); +}; + +FullMenuView.prototype.focusNextPageItem = function() { + // + // Jump to current + up to page size or bottom + // If already at the bottom, jump to top + // + if (this.items.length - 1 === this.focusedItemIndex) { + return this.focusNext(); // will jump to top + } + + const index = Math.min(this.focusedItemIndex + this.maxVisibleItems, this.items.length - 1); + + this.setFocusItemIndex(index); + + return FullMenuView.super_.prototype.focusNextPageItem.call(this); +}; + +FullMenuView.prototype.focusFirst = function() { + this.setFocusItemIndex(0); + return FullMenuView.super_.prototype.focusFirst.call(this); +}; + +FullMenuView.prototype.focusLast = function() { + const index = this.items.length - 1; + + this.setFocusItemIndex(index); + + 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.setItemHorizSpacing = function(itemHorizSpacing) { + FullMenuView.super_.prototype.setItemHorizSpacing.call(this, itemHorizSpacing); + + this.positionCacheExpired = true; +}; 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_view.js b/core/menu_view.js index d9016153..e9c39900 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -38,7 +38,8 @@ 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 || ''; @@ -253,9 +254,18 @@ MenuView.prototype.setItemSpacing = function(itemSpacing) { this.positionCacheExpired = true; }; +MenuView.prototype.setItemHorizSpacing = function(itemHorizSpacing) { + itemSpacing = 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; From 4fe55e1e1b9c4b777238b24091f90e45d8230dd9 Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Sun, 16 Jan 2022 12:12:41 -0600 Subject: [PATCH 052/146] Added support for multiple pages --- core/full_menu_view.js | 224 +++++++++++++++++------- core/menu_view.js | 378 +++++++++++++++++++++-------------------- 2 files changed, 351 insertions(+), 251 deletions(-) diff --git a/core/full_menu_view.js b/core/full_menu_view.js index 87346b9d..3350d567 100644 --- a/core/full_menu_view.js +++ b/core/full_menu_view.js @@ -21,8 +21,15 @@ function FullMenuView(options) { MenuView.call(this, options); + + // Initialize paging + this.pages = []; + this.currentPage = 0; + this.initDefaultWidth(); + + const self = this; // we want page up/page down by default @@ -39,48 +46,111 @@ function FullMenuView(options) { this.dimens.height = Math.min(this.dimens.height, this.client.term.termHeight - this.position.row); } - // Calculate number of items visible after adjusting height - 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; - } - + this.positionCacheExpired = true; }; this.autoAdjustHeightIfEnabled(); + + this.getSpacer = function() { return new Array(self.itemHorizSpacing + 1).join(this.fillChar); } + + this.clearPage = function() { + for (var i = 0; i < this.itemsPerRow; i++) { + let text = `${strUtil.pad(' ', this.dimens.width, this.fillChar, 'left')}`; + self.client.term.write(`${ansi.goto(this.position.row + i, this.position.col)}${text}`); + } + } + this.cachePositions = function() { if (this.positionCacheExpired) { this.autoAdjustHeightIfEnabled(); - var col = self.position.col; - var row = self.position.row; - var spacer = self.getSpacer(); + 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; + } + + var col = this.position.col; + var row = this.position.row; + var spacer = this.getSpacer(); var itemInRow = 0; - for (var i = 0; i < self.items.length; ++i) { + this.viewWindow = { + start: this.focusedItemIndex, + end: this.items.length - 1, // this may be adjusted later + }; + + var pageStart = 0; + + for (var i = 0; i < this.items.length; ++i) { itemInRow++; - self.items[i].row = row; - self.items[i].col = col; + this.items[i].row = row; + this.items[i].col = col; row += this.itemSpacing + 1; - // handle going to next column - if (itemInRow == this.itemsPerRow) { + // have to calculate the max length on the last entry + if (i == this.items.length - 1) { + var maxLength = 0; + for (var j = 0; j < this.itemsPerRow; j++) { + if (this.items[i - j].col != this.items[i].col) { + break; + } + var itemLength = this.items[i - j].text.length; + if (itemLength > maxLength) { + maxLength = itemLength; + } + } + + // set length on each item in the column + for (var 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 + if (col + maxLength + spacer.length + 1 > this.position.col + this.dimens.width) { + // save previous page + this.pages.push({ start: pageStart, end: i - this.itemsPerRow }); + + // fix the last column processed + for (var 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; - row = self.position.row; + // restart row for next column + row = this.position.row; var maxLength = 0; for (var j = 0; j < this.itemsPerRow; j++) { // TODO: handle complex items @@ -92,34 +162,37 @@ function FullMenuView(options) { // set length on each item in the column for (var j = 0; j < this.itemsPerRow; j++) { - self.items[i - j].fixedLength = maxLength; + this.items[i - j].fixedLength = maxLength; + } + + // Check if we have room for this column in the current page + if (col + maxLength > this.position.col + 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 (var j = 0; j < this.itemsPerRow; j++) { + this.items[i - j].col = col; + } + + } // increment the column col += maxLength + spacer.length + 1; } - // also have to calculate the max length on the last column - else if (i == self.items.length - 1) { - var maxLength = 0; - for (var j = 0; j < this.itemsPerRow; j++) { - if (self.items[i - j].col != self.items[i].col) { - break; - } - var itemLength = this.items[i - j].text.length; - if (itemLength > maxLength) { - maxLength = itemLength; - } - } - - // set length on each item in the column - for (var j = 0; j < this.itemsPerRow; j++) { - if (self.items[i - j].col != self.items[i].col) { - break; - } - self.items[i - j].fixedLength = maxLength; - } + // Set the current page if the current item is focused. + if (this.focusedItemIndex === i) { + this.currentPage = this.pages.length; } } } @@ -128,32 +201,32 @@ function FullMenuView(options) { }; this.drawItem = function(index) { - const item = self.items[index]; + const item = this.items[index]; if (!item) { return; } const cached = this.getRenderCacheItem(index, item.focused); if (cached) { - return self.client.term.write(`${ansi.goto(item.row, item.col)}${cached}`); + return this.client.term.write(`${ansi.goto(item.row, item.col)}${cached}`); } let text; let sgr; - if (item.focused && self.hasFocusItems()) { - const focusItem = self.focusItems[index]; + 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 === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); + sgr = this.focusItemFormat ? '' : (index === this.focusedItemIndex ? this.getFocusSGR() : this.getSGR()); } else { - text = strUtil.stylizeString(item.text, item.focused ? self.focusTextStyle : self.textStyle); - sgr = (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); + text = strUtil.stylizeString(item.text, item.focused ? this.focusTextStyle : this.textStyle); + sgr = (index === this.focusedItemIndex ? this.getFocusSGR() : this.getSGR()); } - text = `${sgr}${strUtil.pad(text, this.dimens.width, this.fillChar, this.justify)}`; - self.client.term.write(`${ansi.goto(item.row, item.col)}${text}`); + text = `${sgr}${strUtil.pad(text, this.fixedLength, this.fillChar, this.justify)}`; + this.client.term.write(`${ansi.goto(item.row, item.col)}${text}`); this.setRenderCacheItem(index, text, item.focused); }; } @@ -166,11 +239,6 @@ FullMenuView.prototype.redraw = function() { this.cachePositions(); - // :TODO: rename positionCacheExpired to something that makese sense; combine methods for such - if (this.positionCacheExpired) { - this.autoAdjustHeightIfEnabled(); - this.positionCacheExpired = false; - } // erase old items // :TODO: optimize this: only needed if a item is removed or new max width < old. @@ -189,7 +257,7 @@ FullMenuView.prototype.redraw = function() { } if (this.items.length) { - for (let i = 0; i < this.items.length; ++i) { + 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); } @@ -203,6 +271,12 @@ FullMenuView.prototype.setHeight = function(height) { this.autoAdjustHeight = false; }; +FullMenuView.prototype.setWidth = function(width) { + FullMenuView.super_.prototype.setWidth.call(this, width); + + this.positionCacheExpired = true; +}; + FullMenuView.prototype.setPosition = function(pos) { FullMenuView.super_.prototype.setPosition.call(this, pos); @@ -273,11 +347,16 @@ FullMenuView.prototype.removeItem = function(index) { FullMenuView.prototype.focusNext = function() { if (this.items.length - 1 === this.focusedItemIndex) { + this.clearPage(); this.focusedItemIndex = 0; - - } else { + this.currentPage = 0; + } + else { this.focusedItemIndex++; - + if (this.focusedItemIndex > this.pages[this.currentPage].end) { + this.clearPage(); + this.currentPage++; + } } this.redraw(); @@ -288,10 +367,14 @@ FullMenuView.prototype.focusNext = function() { FullMenuView.prototype.focusPrevious = function() { if (0 === this.focusedItemIndex) { this.focusedItemIndex = this.items.length - 1; - - - } else { + this.currentPage = this.pages.length - 1; + } + else { + this.clearPage(); this.focusedItemIndex--; + if (this.focusedItemIndex < this.pages[this.currentPage].start) { + this.currentPage--; + } } this.redraw(); @@ -303,8 +386,17 @@ FullMenuView.prototype.focusPreviousColumn = function() { this.focusedItemIndex = this.focusedItemIndex - this.itemsPerRow; if (this.focusedItemIndex < 0) { + this.clearPage(); // add the negative index to the end of the list this.focusedItemIndex = this.items.length + this.focusedItemIndex; + // 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(); @@ -319,6 +411,12 @@ FullMenuView.prototype.focusNextColumn = function() { if (this.focusedItemIndex > this.items.length - 1) { // add the overflow to the beginning of the list this.focusedItemIndex = this.focusedItemIndex - this.items.length; + this.currentPage = 0; + this.clearPage(); + } + else if (this.focusedItemIndex > this.pages[this.currentPage].end) { + this.clearPage(); + this.currentPage++; } this.redraw(); diff --git a/core/menu_view.js b/core/menu_view.js index e9c39900..26b3467b 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -2,298 +2,300 @@ 'use strict'; // ENiGMA½ -const View = require('./view.js').View; -const miscUtil = require('./misc_util.js'); -const pipeToAnsi = require('./color_codes.js').pipeToAnsi; +const View = require('./view.js').View; +const miscUtil = require('./misc_util.js'); +const pipeToAnsi = require('./color_codes.js').pipeToAnsi; // deps -const util = require('util'); -const assert = require('assert'); -const _ = require('lodash'); +const util = require('util'); +const assert = require('assert'); +const _ = require('lodash'); -exports.MenuView = MenuView; +exports.MenuView = MenuView; function MenuView(options) { - options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); - options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); + options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); + options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); - View.call(this, options); + View.call(this, options); - this.disablePipe = options.disablePipe || false; + this.disablePipe = options.disablePipe || false; - const self = this; + const self = this; - if(options.items) { - this.setItems(options.items); - } else { - this.items = []; + if (options.items) { + this.setItems(options.items); + } else { + this.items = []; + } + + this.renderCache = {}; + + this.caseInsensitiveHotKeys = miscUtil.valueWithDefault(options.caseInsensitiveHotKeys, true); + + this.setHotKeys(options.hotKeys); + + this.focusedItemIndex = options.focusedItemIndex || 0; + this.focusedItemIndex = this.items.length >= this.focusedItemIndex ? this.focusedItemIndex : 0; + + this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0; + this.itemHorizSpacing = _.isNumber(options.itemHorizSpacing) ? options.itemHorizSpacing : 0; + + // :TODO: probably just replace this with owner draw / pipe codes / etc. more control, less specialization + this.focusPrefix = options.focusPrefix || ''; + this.focusSuffix = options.focusSuffix || ''; + + this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1); + this.justify = options.justify || 'none'; + + this.hasFocusItems = function() { + return !_.isUndefined(self.focusItems); + }; + + this.getHotKeyItemIndex = function(ch) { + if (ch && self.hotKeys) { + const keyIndex = self.hotKeys[self.caseInsensitiveHotKeys ? ch.toLowerCase() : ch]; + if (_.isNumber(keyIndex)) { + return keyIndex; + } } + return -1; + }; - this.renderCache = {}; - - this.caseInsensitiveHotKeys = miscUtil.valueWithDefault(options.caseInsensitiveHotKeys, true); - - this.setHotKeys(options.hotKeys); - - this.focusedItemIndex = options.focusedItemIndex || 0; - this.focusedItemIndex = this.items.length >= this.focusedItemIndex ? this.focusedItemIndex : 0; - - this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0; - this.itemHorizSpacing = _.isNumber(options.itemHorizSpacing) ? options.itemHorizSpacing : 0; - - // :TODO: probably just replace this with owner draw / pipe codes / etc. more control, less specialization - this.focusPrefix = options.focusPrefix || ''; - this.focusSuffix = options.focusSuffix || ''; - - this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1); - this.justify = options.justify || 'none'; - - this.hasFocusItems = function() { - return !_.isUndefined(self.focusItems); - }; - - this.getHotKeyItemIndex = function(ch) { - if(ch && self.hotKeys) { - const keyIndex = self.hotKeys[self.caseInsensitiveHotKeys ? ch.toLowerCase() : ch]; - if(_.isNumber(keyIndex)) { - return keyIndex; - } - } - return -1; - }; - - this.emitIndexUpdate = function() { - self.emit('index update', self.focusedItemIndex); - }; + this.emitIndexUpdate = function() { + self.emit('index update', self.focusedItemIndex); + }; } util.inherits(MenuView, View); MenuView.prototype.setItems = function(items) { - if(Array.isArray(items)) { - this.sorted = false; - this.renderCache = {}; + if (Array.isArray(items)) { + this.sorted = false; + this.renderCache = {}; - // - // Items can be an array of strings or an array of objects. - // - // In the case of objects, items are considered complex and - // may have one or more members that can later be formatted - // against. The default member is 'text'. The member 'data' - // may be overridden to provide a form value other than the - // item's index. - // - // Items can be formatted with 'itemFormat' and 'focusItemFormat' - // - let text; - let stringItem; - this.items = items.map(item => { - stringItem = _.isString(item); - if(stringItem) { - text = item; - } else { - text = item.text || ''; - this.complexItems = true; - } + // + // Items can be an array of strings or an array of objects. + // + // In the case of objects, items are considered complex and + // may have one or more members that can later be formatted + // against. The default member is 'text'. The member 'data' + // may be overridden to provide a form value other than the + // item's index. + // + // Items can be formatted with 'itemFormat' and 'focusItemFormat' + // + let text; + let stringItem; + this.items = items.map(item => { + stringItem = _.isString(item); + if (stringItem) { + text = item; + } else { + text = item.text || ''; + this.complexItems = true; + } - text = this.disablePipe ? text : pipeToAnsi(text, this.client); - return Object.assign({ }, { text }, stringItem ? {} : item); // ensure we have a text member, plus any others - }); + text = this.disablePipe ? text : pipeToAnsi(text, this.client); + return Object.assign({}, { text }, stringItem ? {} : item); // ensure we have a text member, plus any others + }); - if(this.complexItems) { - this.itemFormat = this.itemFormat || '{text}'; - } - - this.invalidateRenderCache(); + if (this.complexItems) { + this.itemFormat = this.itemFormat || '{text}'; } + + this.invalidateRenderCache(); + } }; MenuView.prototype.getRenderCacheItem = function(index, focusItem = false) { - const item = this.renderCache[index]; - return item && item[focusItem ? 'focus' : 'standard']; + const item = this.renderCache[index]; + return item && item[focusItem ? 'focus' : 'standard']; }; MenuView.prototype.removeRenderCacheItem = function(index) { - delete this.renderCache[index]; + delete this.renderCache[index]; }; MenuView.prototype.setRenderCacheItem = function(index, rendered, focusItem = false) { - this.renderCache[index] = this.renderCache[index] || {}; - this.renderCache[index][focusItem ? 'focus' : 'standard'] = rendered; + this.renderCache[index] = this.renderCache[index] || {}; + this.renderCache[index][focusItem ? 'focus' : 'standard'] = rendered; }; MenuView.prototype.invalidateRenderCache = function() { - this.renderCache = {}; + this.renderCache = {}; }; MenuView.prototype.setSort = function(sort) { - if(this.sorted || !Array.isArray(this.items) || 0 === this.items.length) { - return; + if (this.sorted || !Array.isArray(this.items) || 0 === this.items.length) { + return; + } + + const key = true === sort ? 'text' : sort; + if ('text' !== sort && !this.complexItems) { + return; // need a valid sort key + } + + this.items.sort((a, b) => { + const a1 = a[key]; + const b1 = b[key]; + if (!a1) { + return -1; } - - const key = true === sort ? 'text' : sort; - if('text' !== sort && !this.complexItems) { - return; // need a valid sort key + if (!b1) { + return 1; } + return a1.localeCompare(b1, { sensitivity: false, numeric: true }); + }); - this.items.sort( (a, b) => { - const a1 = a[key]; - const b1 = b[key]; - if(!a1) { - return -1; - } - if(!b1) { - return 1; - } - return a1.localeCompare( b1, { sensitivity : false, numeric : true } ); - }); - - this.sorted = true; + this.sorted = true; }; MenuView.prototype.removeItem = function(index) { - this.sorted = false; - this.items.splice(index, 1); + this.sorted = false; + this.items.splice(index, 1); - if(this.focusItems) { - this.focusItems.splice(index, 1); - } + if (this.focusItems) { + this.focusItems.splice(index, 1); + } - if(this.focusedItemIndex >= index) { - this.focusedItemIndex = Math.max(this.focusedItemIndex - 1, 0); - } + if (this.focusedItemIndex >= index) { + this.focusedItemIndex = Math.max(this.focusedItemIndex - 1, 0); + } - this.removeRenderCacheItem(index); + this.removeRenderCacheItem(index); - this.positionCacheExpired = true; + this.positionCacheExpired = true; }; MenuView.prototype.getCount = function() { - return this.items.length; + return this.items.length; }; MenuView.prototype.getItems = function() { - if(this.complexItems) { - return this.items; - } + if (this.complexItems) { + return this.items; + } - return this.items.map( item => { - return item.text; - }); + return this.items.map(item => { + return item.text; + }); }; MenuView.prototype.getItem = function(index) { - if(this.complexItems) { - return this.items[index]; - } + if (this.complexItems) { + return this.items[index]; + } - return this.items[index].text; + return this.items[index].text; }; MenuView.prototype.focusNext = function() { - this.emitIndexUpdate(); + this.emitIndexUpdate(); }; MenuView.prototype.focusPrevious = function() { - this.emitIndexUpdate(); + this.emitIndexUpdate(); }; MenuView.prototype.focusNextPageItem = function() { - this.emitIndexUpdate(); + this.emitIndexUpdate(); }; MenuView.prototype.focusPreviousPageItem = function() { - this.emitIndexUpdate(); + this.emitIndexUpdate(); }; MenuView.prototype.focusFirst = function() { - this.emitIndexUpdate(); + this.emitIndexUpdate(); }; MenuView.prototype.focusLast = function() { - this.emitIndexUpdate(); + this.emitIndexUpdate(); }; MenuView.prototype.setFocusItemIndex = function(index) { - this.focusedItemIndex = index; + this.focusedItemIndex = index; }; MenuView.prototype.onKeyPress = function(ch, key) { - const itemIndex = this.getHotKeyItemIndex(ch); - if(itemIndex >= 0) { - this.setFocusItemIndex(itemIndex); + const itemIndex = this.getHotKeyItemIndex(ch); + if (itemIndex >= 0) { + this.setFocusItemIndex(itemIndex); - if(true === this.hotKeySubmit) { - this.emit('action', 'accept'); - } + if (true === this.hotKeySubmit) { + this.emit('action', 'accept'); } + } - MenuView.super_.prototype.onKeyPress.call(this, ch, key); + MenuView.super_.prototype.onKeyPress.call(this, ch, key); }; MenuView.prototype.setFocusItems = function(items) { - const self = this; + const self = this; - if(items) { - this.focusItems = []; - items.forEach( itemText => { - this.focusItems.push( - { - text : self.disablePipe ? itemText : pipeToAnsi(itemText, self.client) - } - ); - }); - } + if (items) { + this.focusItems = []; + items.forEach(itemText => { + this.focusItems.push( + { + text: self.disablePipe ? itemText : pipeToAnsi(itemText, self.client) + } + ); + }); + } }; MenuView.prototype.setItemSpacing = function(itemSpacing) { - itemSpacing = parseInt(itemSpacing); - assert(_.isNumber(itemSpacing)); + itemSpacing = parseInt(itemSpacing); + assert(_.isNumber(itemSpacing)); - this.itemSpacing = itemSpacing; - this.positionCacheExpired = true; + this.itemSpacing = itemSpacing; + this.positionCacheExpired = true; }; MenuView.prototype.setItemHorizSpacing = function(itemHorizSpacing) { - itemSpacing = parseInt(itemHorizSpacing); - assert(_.isNumber(itemHorizSpacing)); + itemSpacing = parseInt(itemHorizSpacing); + assert(_.isNumber(itemHorizSpacing)); - this.itemHorizSpacing = itemHorizSpacing; - this.positionCacheExpired = true; + 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 'hotKeySubmit' : this.hotKeySubmit = value; break; - case 'justify' : this.justify = value; break; - case 'focusItemIndex' : this.focusedItemIndex = value; break; + 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 'hotKeySubmit': this.hotKeySubmit = value; break; + case 'justify': this.justify = value; break; + case 'focusItemIndex': this.focusedItemIndex = value; break; - case 'itemFormat' : - case 'focusItemFormat' : - this[propName] = 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; - } + case 'sort': this.setSort(value); break; + } - MenuView.super_.prototype.setPropertyValue.call(this, propName, value); + MenuView.super_.prototype.setPropertyValue.call(this, propName, value); }; MenuView.prototype.setHotKeys = function(hotKeys) { - if(_.isObject(hotKeys)) { - if(this.caseInsensitiveHotKeys) { - this.hotKeys = {}; - for(var key in hotKeys) { - this.hotKeys[key.toLowerCase()] = hotKeys[key]; - } - } else { - this.hotKeys = hotKeys; - } + if (_.isObject(hotKeys)) { + if (this.caseInsensitiveHotKeys) { + this.hotKeys = {}; + for (var key in hotKeys) { + this.hotKeys[key.toLowerCase()] = hotKeys[key]; + } + } else { + this.hotKeys = hotKeys; } + } }; From d77de9c388305cc79a715c22defa2db9559013bb Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Wed, 19 Jan 2022 13:07:02 -0600 Subject: [PATCH 053/146] Cleaned up issues with length --- core/full_menu_view.js | 49 +++++++++++++++++++++++++++++++----------- core/menu_view.js | 10 +++++++++ 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/core/full_menu_view.js b/core/full_menu_view.js index 3350d567..7f9914a9 100644 --- a/core/full_menu_view.js +++ b/core/full_menu_view.js @@ -59,7 +59,7 @@ function FullMenuView(options) { this.clearPage = function() { - for (var i = 0; i < this.itemsPerRow; i++) { + for (var i = 0; i < this.dimens.height; i++) { let text = `${strUtil.pad(' ', this.dimens.width, this.fillChar, 'left')}`; self.client.term.write(`${ansi.goto(this.position.row + i, this.position.col)}${text}`); } @@ -88,11 +88,7 @@ function FullMenuView(options) { var spacer = this.getSpacer(); var itemInRow = 0; - - this.viewWindow = { - start: this.focusedItemIndex, - end: this.items.length - 1, // this may be adjusted later - }; + var itemInCol = 0; var pageStart = 0; @@ -100,6 +96,7 @@ function FullMenuView(options) { itemInRow++; this.items[i].row = row; this.items[i].col = col; + this.items[i].itemInRow = itemInRow; row += this.itemSpacing + 1; @@ -126,7 +123,8 @@ function FullMenuView(options) { // Check if we have room for this column - if (col + maxLength + spacer.length + 1 > this.position.col + this.dimens.width) { + // skip for column 0, we need at least one + if (itemInCol != 0 && (col + maxLength + spacer.length + 1 > this.position.col + this.dimens.width)) { // save previous page this.pages.push({ start: pageStart, end: i - this.itemsPerRow }); @@ -166,7 +164,8 @@ function FullMenuView(options) { } // Check if we have room for this column in the current page - if (col + maxLength > this.position.col + this.dimens.width) { + // skip for first column, we need at least one + if (itemInCol != 0 && (col + maxLength > this.position.col + this.dimens.width)) { // save previous page this.pages.push({ start: pageStart, end: i - this.itemsPerRow }); @@ -183,10 +182,14 @@ function FullMenuView(options) { } + + + } // increment the column col += maxLength + spacer.length + 1; + itemInCol++; } @@ -225,6 +228,11 @@ function FullMenuView(options) { 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; + } + text = `${sgr}${strUtil.pad(text, this.fixedLength, this.fillChar, this.justify)}`; this.client.term.write(`${ansi.goto(item.row, item.col)}${text}`); this.setRenderCacheItem(index, text, item.focused); @@ -277,6 +285,13 @@ FullMenuView.prototype.setWidth = function(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); @@ -366,13 +381,14 @@ FullMenuView.prototype.focusNext = function() { FullMenuView.prototype.focusPrevious = function() { if (0 === this.focusedItemIndex) { + this.clearPage(); this.focusedItemIndex = this.items.length - 1; this.currentPage = this.pages.length - 1; } else { - this.clearPage(); this.focusedItemIndex--; if (this.focusedItemIndex < this.pages[this.currentPage].start) { + this.clearPage(); this.currentPage--; } } @@ -384,11 +400,20 @@ FullMenuView.prototype.focusPrevious = function() { FullMenuView.prototype.focusPreviousColumn = function() { + var currentRow = this.items[this.focusedItemIndex].itemInRow; this.focusedItemIndex = this.focusedItemIndex - this.itemsPerRow; if (this.focusedItemIndex < 0) { this.clearPage(); + var 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; + } // add the negative index to the end of the list - this.focusedItemIndex = this.items.length + this.focusedItemIndex; + // this.focusedItemIndex = this.items.length + this.focusedItemIndex; // set to last page this.currentPage = this.pages.length - 1; } @@ -407,10 +432,10 @@ FullMenuView.prototype.focusPreviousColumn = function() { FullMenuView.prototype.focusNextColumn = function() { + var currentRow = this.items[this.focusedItemIndex].itemInRow; this.focusedItemIndex = this.focusedItemIndex + this.itemsPerRow; if (this.focusedItemIndex > this.items.length - 1) { - // add the overflow to the beginning of the list - this.focusedItemIndex = this.focusedItemIndex - this.items.length; + this.focusedItemIndex = currentRow - 1; this.currentPage = 0; this.clearPage(); } diff --git a/core/menu_view.js b/core/menu_view.js index 26b3467b..e9c878df 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -69,6 +69,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; @@ -269,6 +278,7 @@ MenuView.prototype.setPropertyValue = function(propName, value) { 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 'focusItemIndex': this.focusedItemIndex = value; break; From c82497b75ea5f64d495da84bfcab1443c55f25f3 Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Wed, 19 Jan 2022 14:36:43 -0600 Subject: [PATCH 054/146] Fixed justification --- core/full_menu_view.js | 20 ++++++++++++-------- core/menu_view.js | 9 +++++++-- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/core/full_menu_view.js b/core/full_menu_view.js index 7f9914a9..2869c812 100644 --- a/core/full_menu_view.js +++ b/core/full_menu_view.js @@ -124,9 +124,9 @@ function FullMenuView(options) { // Check if we have room for this column // skip for column 0, we need at least one - if (itemInCol != 0 && (col + maxLength + spacer.length + 1 > this.position.col + this.dimens.width)) { + if (itemInCol != 0 && (col + maxLength > this.dimens.width)) { // save previous page - this.pages.push({ start: pageStart, end: i - this.itemsPerRow }); + this.pages.push({ start: pageStart, end: i - itemInRow }); // fix the last column processed for (var j = 0; j < this.itemsPerRow; j++) { @@ -165,7 +165,7 @@ function FullMenuView(options) { // 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.position.col + this.dimens.width)) { + if (itemInCol != 0 && (col + maxLength > this.dimens.width)) { // save previous page this.pages.push({ start: pageStart, end: i - this.itemsPerRow }); @@ -181,10 +181,6 @@ function FullMenuView(options) { this.items[i - j].col = col; } - - - - } // increment the column @@ -233,7 +229,9 @@ function FullMenuView(options) { text = strUtil.renderSubstr(text, 0, this.dimens.width - (item.col + this.textOverflow.length)) + this.textOverflow; } - text = `${sgr}${strUtil.pad(text, this.fixedLength, this.fillChar, this.justify)}`; + let padLength = Math.min(item.fixedLength + 1, this.dimens.width); + + text = `${sgr}${strUtil.pad(text, padLength, this.fillChar, this.justify)}`; this.client.term.write(`${ansi.goto(item.row, item.col)}${text}`); this.setRenderCacheItem(index, text, item.focused); }; @@ -508,6 +506,12 @@ FullMenuView.prototype.setItemSpacing = function(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); diff --git a/core/menu_view.js b/core/menu_view.js index e9c878df..ea0b0b4d 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -46,7 +46,6 @@ function MenuView(options) { 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); @@ -280,7 +279,7 @@ MenuView.prototype.setPropertyValue = function(propName, value) { 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 'focusItemIndex': this.focusedItemIndex = value; break; case 'itemFormat': @@ -296,6 +295,12 @@ MenuView.prototype.setPropertyValue = function(propName, value) { MenuView.super_.prototype.setPropertyValue.call(this, propName, value); }; +MenuView.prototype.setJustify = function(justify) { + this.justify = justify; + this.invalidateRenderCache(); + this.positionCacheExpired = true; +}; + MenuView.prototype.setHotKeys = function(hotKeys) { if (_.isObject(hotKeys)) { if (this.caseInsensitiveHotKeys) { From c4ed09a8bb6edf7126b70c6a1cb214c847e1269c Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Wed, 19 Jan 2022 15:11:33 -0600 Subject: [PATCH 055/146] Fixed fill character --- core/full_menu_view.js | 17 ++++++++++++++--- core/menu_view.js | 8 +++++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/core/full_menu_view.js b/core/full_menu_view.js index 2869c812..30b384b4 100644 --- a/core/full_menu_view.js +++ b/core/full_menu_view.js @@ -59,8 +59,9 @@ function FullMenuView(options) { this.clearPage = function() { + console.log("Clear page called with f: ~%s~, h: %d, w: %d, r: %d, c: %d", this.fillChar, this.dimens.height, this.dimens.width, this.position.row, this.position.col); for (var i = 0; i < this.dimens.height; i++) { - let text = `${strUtil.pad(' ', this.dimens.width, this.fillChar, 'left')}`; + let text = `${strUtil.pad(this.fillChar, this.dimens.width, this.fillChar, 'left')}`; self.client.term.write(`${ansi.goto(this.position.row + i, this.position.col)}${text}`); } } @@ -184,7 +185,7 @@ function FullMenuView(options) { } // increment the column - col += maxLength + spacer.length + 1; + col += maxLength + spacer.length; itemInCol++; } @@ -231,8 +232,9 @@ function FullMenuView(options) { let padLength = Math.min(item.fixedLength + 1, this.dimens.width); - text = `${sgr}${strUtil.pad(text, padLength, this.fillChar, this.justify)}`; + text = `${sgr}${strUtil.pad(text, padLength, this.fillChar, this.justify)}${this.getSGR()}`; this.client.term.write(`${ansi.goto(item.row, item.col)}${text}`); + // this.client.term.write(`${ansi.goto(item.row, item.col)}${text}${this.getSpacer()}`); this.setRenderCacheItem(index, text, item.focused); }; } @@ -275,6 +277,7 @@ FullMenuView.prototype.setHeight = function(height) { this.positionCacheExpired = true; this.autoAdjustHeight = false; + this.clearPage(); }; FullMenuView.prototype.setWidth = function(width) { @@ -296,6 +299,13 @@ FullMenuView.prototype.setPosition = function(pos) { this.positionCacheExpired = true; }; +FullMenuView.prototype.setFillChar = function(fillChar) { + FullMenuView.super_.prototype.setFillChar.call(this, fillChar); + + this.clearPage(); + this.redraw(); +}; + FullMenuView.prototype.setFocus = function(focused) { FullMenuView.super_.prototype.setFocus.call(this, focused); @@ -354,6 +364,7 @@ FullMenuView.prototype.removeItem = function(index) { } FullMenuView.super_.prototype.removeItem.call(this, index); + this.positionCacheExpired = true; }; // :TODO: Apply draw optimizaitons when only two items need drawn vs entire view! diff --git a/core/menu_view.js b/core/menu_view.js index ea0b0b4d..f2bca3cf 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -263,7 +263,7 @@ MenuView.prototype.setItemSpacing = function(itemSpacing) { }; MenuView.prototype.setItemHorizSpacing = function(itemHorizSpacing) { - itemSpacing = parseInt(itemHorizSpacing); + itemHorizSpacing = parseInt(itemHorizSpacing); assert(_.isNumber(itemHorizSpacing)); this.itemHorizSpacing = itemHorizSpacing; @@ -280,6 +280,7 @@ MenuView.prototype.setPropertyValue = function(propName, value) { case 'textOverflow': this.setTextOverflow(value); break; case 'hotKeySubmit': this.hotKeySubmit = value; break; case 'justify': this.setJustify(value); break; + case 'fillChar': this.setFillChar(value); break; case 'focusItemIndex': this.focusedItemIndex = value; break; case 'itemFormat': @@ -295,6 +296,11 @@ 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(); From 358a778b0a3f0d70a38cc55d094856331ec2fa6b Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Fri, 21 Jan 2022 13:53:32 -0600 Subject: [PATCH 056/146] Fixed uneccessary call to setFocus --- core/full_menu_view.js | 63 +++++++++++++++++++----------------------- core/view.js | 14 +++++++--- 2 files changed, 39 insertions(+), 38 deletions(-) diff --git a/core/full_menu_view.js b/core/full_menu_view.js index 30b384b4..e3c6c8de 100644 --- a/core/full_menu_view.js +++ b/core/full_menu_view.js @@ -59,15 +59,26 @@ function FullMenuView(options) { this.clearPage = function() { - console.log("Clear page called with f: ~%s~, h: %d, w: %d, r: %d, c: %d", this.fillChar, this.dimens.height, this.dimens.width, this.position.row, this.position.col); + var width = this.dimens.width; + if (this.oldDimens) { + if (this.oldDimens.width > width) { + width = this.oldDimens.width; + } + delete this.oldDimens; + } + for (var i = 0; i < this.dimens.height; i++) { - let text = `${strUtil.pad(this.fillChar, this.dimens.width, this.fillChar, 'left')}`; - self.client.term.write(`${ansi.goto(this.position.row + i, this.position.col)}${text}`); + let text = `${strUtil.pad(this.fillChar, width, this.fillChar, 'left')}`; + self.client.term.write(`${ansi.goto(this.position.row + i, this.position.col)}${this.getSGR()}${text}`); } } this.cachePositions = function() { if (this.positionCacheExpired) { + // first, clear the page + this.clearPage(); + + this.autoAdjustHeightIfEnabled(); this.pages = []; // reset @@ -234,7 +245,6 @@ function FullMenuView(options) { text = `${sgr}${strUtil.pad(text, padLength, this.fillChar, this.justify)}${this.getSGR()}`; this.client.term.write(`${ansi.goto(item.row, item.col)}${text}`); - // this.client.term.write(`${ansi.goto(item.row, item.col)}${text}${this.getSpacer()}`); this.setRenderCacheItem(index, text, item.focused); }; } @@ -242,28 +252,20 @@ function FullMenuView(options) { util.inherits(FullMenuView, MenuView); +//TODO: FIXME +function debugLine(message) { + let e = new Error(); + let frame = e.stack.split("\n")[3]; // change to 3 for grandparent func + let lineNumber = frame.split(":").reverse()[1]; + let functionName = frame.split(" ")[5]; + return functionName + ":" + lineNumber + " " + message; +} + FullMenuView.prototype.redraw = function() { FullMenuView.super_.prototype.redraw.call(this); this.cachePositions(); - - // erase old items - // :TODO: optimize this: only needed if a item is removed or new max width < old. - if (this.oldDimens) { - const blank = new Array(Math.max(this.oldDimens.width, this.dimens.width)).join(' '); - let seq = ansi.goto(this.position.row, this.position.col) + this.getSGR() + blank; - let row = this.position.row + 1; - const endRow = (row + this.oldDimens.height) - 2; - - while (row <= endRow) { - seq += ansi.goto(row, this.position.col) + blank; - row += 1; - } - this.client.term.write(seq); - delete this.oldDimens; - } - 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; @@ -273,14 +275,17 @@ FullMenuView.prototype.redraw = function() { }; FullMenuView.prototype.setHeight = function(height) { + this.oldDimens = Object.assign({}, this.dimens); + FullMenuView.super_.prototype.setHeight.call(this, height); this.positionCacheExpired = true; this.autoAdjustHeight = false; - this.clearPage(); }; FullMenuView.prototype.setWidth = function(width) { + this.oldDimens = Object.assign({}, this.dimens); + FullMenuView.super_.prototype.setWidth.call(this, width); this.positionCacheExpired = true; @@ -299,23 +304,16 @@ FullMenuView.prototype.setPosition = function(pos) { this.positionCacheExpired = true; }; -FullMenuView.prototype.setFillChar = function(fillChar) { - FullMenuView.super_.prototype.setFillChar.call(this, fillChar); - - this.clearPage(); - this.redraw(); -}; - 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 - - this.redraw(); }; FullMenuView.prototype.onKeyPress = function(ch, key) { @@ -421,8 +419,6 @@ FullMenuView.prototype.focusPreviousColumn = function() { // can't go to same column, so go to last item this.focusedItemIndex = this.items.length - 1; } - // add the negative index to the end of the list - // this.focusedItemIndex = this.items.length + this.focusedItemIndex; // set to last page this.currentPage = this.pages.length - 1; } @@ -459,7 +455,6 @@ FullMenuView.prototype.focusNextColumn = function() { FullMenuView.super_.prototype.focusNext.call(this); }; - FullMenuView.prototype.focusPreviousPageItem = function() { // // Jump to current - up to page size or top diff --git a/core/view.js b/core/view.js index fdf78916..c31ccca1 100644 --- a/core/view.js +++ b/core/view.js @@ -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(); }; From 22808cf2e2f33dc0679eee7222d7131ec9b40b3a Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Fri, 21 Jan 2022 14:27:19 -0600 Subject: [PATCH 057/146] Fixed page up, page down, home, and end --- core/full_menu_view.js | 59 ++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/core/full_menu_view.js b/core/full_menu_view.js index e3c6c8de..bbdda4b6 100644 --- a/core/full_menu_view.js +++ b/core/full_menu_view.js @@ -251,16 +251,6 @@ function FullMenuView(options) { util.inherits(FullMenuView, MenuView); - -//TODO: FIXME -function debugLine(message) { - let e = new Error(); - let frame = e.stack.split("\n")[3]; // change to 3 for grandparent func - let lineNumber = frame.split(":").reverse()[1]; - let functionName = frame.split(" ")[5]; - return functionName + ":" + lineNumber + " " + message; -} - FullMenuView.prototype.redraw = function() { FullMenuView.super_.prototype.redraw.call(this); @@ -365,8 +355,6 @@ FullMenuView.prototype.removeItem = function(index) { this.positionCacheExpired = true; }; -// :TODO: Apply draw optimizaitons when only two items need drawn vs entire view! - FullMenuView.prototype.focusNext = function() { if (this.items.length - 1 === this.focusedItemIndex) { this.clearPage(); @@ -456,47 +444,56 @@ FullMenuView.prototype.focusNextColumn = function() { }; FullMenuView.prototype.focusPreviousPageItem = function() { - // - // Jump to current - up to page size or top - // If already at the top, jump to bottom - // - if (0 === this.focusedItemIndex) { - return this.focusPrevious(); // will jump to bottom + + // handle first page + if (this.currentPage == 0) { + // Do nothing, page up shouldn't go down on last page + return; } - const index = Math.max(this.focusedItemIndex - this.dimens.height, 0); + this.currentPage--; + this.focusedItemIndex = this.pages[this.currentPage].start; + this.clearPage(); - this.setFocusItemIndex(index); + this.redraw(); return FullMenuView.super_.prototype.focusPreviousPageItem.call(this); }; FullMenuView.prototype.focusNextPageItem = function() { - // - // Jump to current + up to page size or bottom - // If already at the bottom, jump to top - // - if (this.items.length - 1 === this.focusedItemIndex) { - return this.focusNext(); // will jump to top + + // handle last page + if (this.currentPage == this.pages.length - 1) { + // Do nothing, page up shouldn't go down on last page + return; } - const index = Math.min(this.focusedItemIndex + this.maxVisibleItems, this.items.length - 1); + this.currentPage++; + this.focusedItemIndex = this.pages[this.currentPage].start; + this.clearPage(); - this.setFocusItemIndex(index); + this.redraw(); return FullMenuView.super_.prototype.focusNextPageItem.call(this); }; FullMenuView.prototype.focusFirst = function() { - this.setFocusItemIndex(0); + + this.currentPage = 0; + this.focusedItemIndex = 0; + this.clearPage(); + + this.redraw(); return FullMenuView.super_.prototype.focusFirst.call(this); }; FullMenuView.prototype.focusLast = function() { - const index = this.items.length - 1; - this.setFocusItemIndex(index); + this.currentPage = this.pages.length - 1; + this.focusedItemIndex = this.pages[this.currentPage].end; + this.clearPage(); + this.redraw(); return FullMenuView.super_.prototype.focusLast.call(this); }; From 9ce599034353cf0f6a2e1dd90e9340f640de51c0 Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Fri, 21 Jan 2022 16:09:41 -0600 Subject: [PATCH 058/146] Documentation WIP --- docs/_includes/nav.md | 2 + docs/art/mci.md | 3 +- docs/art/views/full_menu_view.md | 231 +++++++++++++++++++++++++++++++ 3 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 docs/art/views/full_menu_view.md diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md index 42cc7bff..cd635928 100644 --- a/docs/_includes/nav.md +++ b/docs/_includes/nav.md @@ -49,6 +49,8 @@ - [General]({{ site.baseurl }}{% link art/general.md %}) - [Themes]({{ site.baseurl }}{% link art/themes.md %}) - [MCI Codes]({{ site.baseurl }}{% link art/mci.md %}) + - Views + - [Full Menu]({{ site.baseurl }}{% link art/views/full_menu_view.md %}) - Servers - Login Servers diff --git a/docs/art/mci.md b/docs/art/mci.md index 174643a4..79135554 100644 --- a/docs/art/mci.md +++ b/docs/art/mci.md @@ -123,6 +123,7 @@ a Vertical Menu (`%VM`): Old-school BBSers may recognize this as a lightbar menu | `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 | +| `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 | | `TM` | Toggle Menu | A toggle menu | Commonly used for Yes/No style input | | `KE` | Key Entry | A *single* key input control | Think hotkeys | @@ -233,4 +234,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/views/full_menu_view.md b/docs/art/views/full_menu_view.md new file mode 100644 index 00000000..27f782fd --- /dev/null +++ b/docs/art/views/full_menu_view.md @@ -0,0 +1,231 @@ +--- +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 [Art](../art.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 [Art](../art.md) | +| `focusTextStyle` | Sets focus text style. See **Text Styles** in [Art](../art.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 [Art](../art.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 [Art](../art.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 + } +] +``` + +### 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 + +#### Configuration fragment + +``` +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 + } + ] +} + +``` + +#### Example + +![Example](../assets/images/text-format-example1.png "Text Format") + +### A simple horizontal menu - similar to HM + +#### Configuration fragment + +``` +FM2: { + focus: true + height: 1 + width: 60 // set as desired + submit: true + argName: navSelect + items: [ + "prev", "next", "details", "toggle queue", "rate", "help", "quit" + ] +} +``` + +#### Example + +![Example](../assets/images/text-format-example1.png "Text Format") + +### A multi-column navigation menu with hotkeys + + +#### Configuration fragment + +``` +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 + } + ] +} +``` + +#### Example + +![Example](../assets/images/text-format-example1.png "Text Format") + From e359891d046b22751c06c2f77a4e5e38e906984b Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Fri, 21 Jan 2022 16:56:50 -0600 Subject: [PATCH 059/146] Example gif --- docs/art/views/full_menu_view.md | 2 +- docs/assets/images/full_menu_view_example3.gif | Bin 0 -> 28011 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 docs/assets/images/full_menu_view_example3.gif diff --git a/docs/art/views/full_menu_view.md b/docs/art/views/full_menu_view.md index 27f782fd..bfac75f7 100644 --- a/docs/art/views/full_menu_view.md +++ b/docs/art/views/full_menu_view.md @@ -227,5 +227,5 @@ FM1: { #### Example -![Example](../assets/images/text-format-example1.png "Text Format") +![Example](../../assets/images/full_menu_view_example3.gif "Multi column menu") diff --git a/docs/assets/images/full_menu_view_example3.gif b/docs/assets/images/full_menu_view_example3.gif new file mode 100644 index 0000000000000000000000000000000000000000..b57375fade714995f74fea72d9902dd31dccf7d0 GIT binary patch literal 28011 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 BMRNcE literal 0 HcmV?d00001 From 2c2951866b62afdab73155f44ef82f8bfe145b17 Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Sun, 23 Jan 2022 10:17:23 -0600 Subject: [PATCH 060/146] Additional examples --- docs/art/views/full_menu_view.md | 30 +++++++++--------- .../assets/images/full_menu_view_example1.gif | Bin 0 -> 7198 bytes .../assets/images/full_menu_view_example2.gif | Bin 0 -> 7832 bytes 3 files changed, 15 insertions(+), 15 deletions(-) create mode 100644 docs/assets/images/full_menu_view_example1.gif create mode 100644 docs/assets/images/full_menu_view_example2.gif diff --git a/docs/art/views/full_menu_view.md b/docs/art/views/full_menu_view.md index bfac75f7..03db2cbb 100644 --- a/docs/art/views/full_menu_view.md +++ b/docs/art/views/full_menu_view.md @@ -80,7 +80,10 @@ The `textOverflow` option is used to specify what happens when a text string is ### A simple vertical menu - similar to VM -#### Configuration fragment +![Example](../../assets/images/full_menu_view_example1.gif "Vertical menu") + +
+Configuration fragment (expand to view) ``` FM1: { @@ -108,14 +111,14 @@ FM1: { } ``` - -#### Example - -![Example](../assets/images/text-format-example1.png "Text Format") +
### A simple horizontal menu - similar to HM -#### Configuration fragment +![Example](../../assets/images/full_menu_view_example2.gif "Horizontal menu") + +
+Configuration fragment (expand to view) ``` FM2: { @@ -129,15 +132,15 @@ FM2: { ] } ``` - -#### Example - -![Example](../assets/images/text-format-example1.png "Text Format") +
### A multi-column navigation menu with hotkeys -#### Configuration fragment +![Example](../../assets/images/full_menu_view_example3.gif "Multi column menu") + +
+Configuration fragment (expand to view) ``` FM1: { @@ -224,8 +227,5 @@ FM1: { ] } ``` - -#### Example - -![Example](../../assets/images/full_menu_view_example3.gif "Multi column menu") +
diff --git a/docs/assets/images/full_menu_view_example1.gif b/docs/assets/images/full_menu_view_example1.gif new file mode 100644 index 0000000000000000000000000000000000000000..7366d8130dbada70680437908d58f1ad98b2c0aa GIT binary patch literal 7198 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 qQnD>jAWeF4 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 literal 0 HcmV?d00001 From 0f4cecc97d41c7122b06584d00431b8a22c88867 Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Sun, 23 Jan 2022 10:46:09 -0600 Subject: [PATCH 061/146] Fixed formatting on menu_view.js --- core/menu_view.js | 388 +++++++++++++++++++++++----------------------- 1 file changed, 194 insertions(+), 194 deletions(-) diff --git a/core/menu_view.js b/core/menu_view.js index f2bca3cf..9c750aba 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -2,68 +2,68 @@ 'use strict'; // ENiGMA½ -const View = require('./view.js').View; -const miscUtil = require('./misc_util.js'); -const pipeToAnsi = require('./color_codes.js').pipeToAnsi; +const View = require('./view.js').View; +const miscUtil = require('./misc_util.js'); +const pipeToAnsi = require('./color_codes.js').pipeToAnsi; // deps -const util = require('util'); -const assert = require('assert'); -const _ = require('lodash'); +const util = require('util'); +const assert = require('assert'); +const _ = require('lodash'); -exports.MenuView = MenuView; +exports.MenuView = MenuView; function MenuView(options) { - options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); - options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); + options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); + options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); - View.call(this, options); + View.call(this, options); - this.disablePipe = options.disablePipe || false; + this.disablePipe = options.disablePipe || false; - const self = this; + const self = this; - if (options.items) { - this.setItems(options.items); - } else { - this.items = []; - } - - this.renderCache = {}; - - this.caseInsensitiveHotKeys = miscUtil.valueWithDefault(options.caseInsensitiveHotKeys, true); - - this.setHotKeys(options.hotKeys); - - this.focusedItemIndex = options.focusedItemIndex || 0; - this.focusedItemIndex = this.items.length >= this.focusedItemIndex ? this.focusedItemIndex : 0; - - this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0; - this.itemHorizSpacing = _.isNumber(options.itemHorizSpacing) ? options.itemHorizSpacing : 0; - - // :TODO: probably just replace this with owner draw / pipe codes / etc. more control, less specialization - this.focusPrefix = options.focusPrefix || ''; - this.focusSuffix = options.focusSuffix || ''; - - this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1); - - this.hasFocusItems = function() { - return !_.isUndefined(self.focusItems); - }; - - this.getHotKeyItemIndex = function(ch) { - if (ch && self.hotKeys) { - const keyIndex = self.hotKeys[self.caseInsensitiveHotKeys ? ch.toLowerCase() : ch]; - if (_.isNumber(keyIndex)) { - return keyIndex; - } + if(options.items) { + this.setItems(options.items); + } else { + this.items = []; } - return -1; - }; - this.emitIndexUpdate = function() { - self.emit('index update', self.focusedItemIndex); - }; + this.renderCache = {}; + + this.caseInsensitiveHotKeys = miscUtil.valueWithDefault(options.caseInsensitiveHotKeys, true); + + this.setHotKeys(options.hotKeys); + + this.focusedItemIndex = options.focusedItemIndex || 0; + this.focusedItemIndex = this.items.length >= this.focusedItemIndex ? this.focusedItemIndex : 0; + + this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0; + this.itemHorizSpacing = _.isNumber(options.itemHorizSpacing) ? options.itemHorizSpacing : 0; + + // :TODO: probably just replace this with owner draw / pipe codes / etc. more control, less specialization + this.focusPrefix = options.focusPrefix || ''; + this.focusSuffix = options.focusSuffix || ''; + + this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1); + + this.hasFocusItems = function() { + return !_.isUndefined(self.focusItems); + }; + + this.getHotKeyItemIndex = function(ch) { + if(ch && self.hotKeys) { + const keyIndex = self.hotKeys[self.caseInsensitiveHotKeys ? ch.toLowerCase() : ch]; + if(_.isNumber(keyIndex)) { + return keyIndex; + } + } + return -1; + }; + + this.emitIndexUpdate = function() { + self.emit('index update', self.focusedItemIndex); + }; } util.inherits(MenuView, View); @@ -74,226 +74,226 @@ MenuView.prototype.setTextOverflow = function(overflow) { } MenuView.prototype.hasTextOverflow = function() { - return this.textOverflow !== undefined; + return this.textOverflow != undefined; } MenuView.prototype.setItems = function(items) { - if (Array.isArray(items)) { - this.sorted = false; - this.renderCache = {}; + if(Array.isArray(items)) { + this.sorted = false; + this.renderCache = {}; - // - // Items can be an array of strings or an array of objects. - // - // In the case of objects, items are considered complex and - // may have one or more members that can later be formatted - // against. The default member is 'text'. The member 'data' - // may be overridden to provide a form value other than the - // item's index. - // - // Items can be formatted with 'itemFormat' and 'focusItemFormat' - // - let text; - let stringItem; - this.items = items.map(item => { - stringItem = _.isString(item); - if (stringItem) { - text = item; - } else { - text = item.text || ''; - this.complexItems = true; - } + // + // Items can be an array of strings or an array of objects. + // + // In the case of objects, items are considered complex and + // may have one or more members that can later be formatted + // against. The default member is 'text'. The member 'data' + // may be overridden to provide a form value other than the + // item's index. + // + // Items can be formatted with 'itemFormat' and 'focusItemFormat' + // + let text; + let stringItem; + this.items = items.map(item => { + stringItem = _.isString(item); + if(stringItem) { + text = item; + } else { + text = item.text || ''; + this.complexItems = true; + } - text = this.disablePipe ? text : pipeToAnsi(text, this.client); - return Object.assign({}, { text }, stringItem ? {} : item); // ensure we have a text member, plus any others - }); + text = this.disablePipe ? text : pipeToAnsi(text, this.client); + return Object.assign({ }, { text }, stringItem ? {} : item); // ensure we have a text member, plus any others + }); - if (this.complexItems) { - this.itemFormat = this.itemFormat || '{text}'; + if(this.complexItems) { + this.itemFormat = this.itemFormat || '{text}'; + } + + this.invalidateRenderCache(); } - - this.invalidateRenderCache(); - } }; MenuView.prototype.getRenderCacheItem = function(index, focusItem = false) { - const item = this.renderCache[index]; - return item && item[focusItem ? 'focus' : 'standard']; + const item = this.renderCache[index]; + return item && item[focusItem ? 'focus' : 'standard']; }; MenuView.prototype.removeRenderCacheItem = function(index) { - delete this.renderCache[index]; + delete this.renderCache[index]; }; MenuView.prototype.setRenderCacheItem = function(index, rendered, focusItem = false) { - this.renderCache[index] = this.renderCache[index] || {}; - this.renderCache[index][focusItem ? 'focus' : 'standard'] = rendered; + this.renderCache[index] = this.renderCache[index] || {}; + this.renderCache[index][focusItem ? 'focus' : 'standard'] = rendered; }; MenuView.prototype.invalidateRenderCache = function() { - this.renderCache = {}; + this.renderCache = {}; }; MenuView.prototype.setSort = function(sort) { - if (this.sorted || !Array.isArray(this.items) || 0 === this.items.length) { - return; - } - - const key = true === sort ? 'text' : sort; - if ('text' !== sort && !this.complexItems) { - return; // need a valid sort key - } - - this.items.sort((a, b) => { - const a1 = a[key]; - const b1 = b[key]; - if (!a1) { - return -1; + if(this.sorted || !Array.isArray(this.items) || 0 === this.items.length) { + return; } - if (!b1) { - return 1; - } - return a1.localeCompare(b1, { sensitivity: false, numeric: true }); - }); - this.sorted = true; + const key = true === sort ? 'text' : sort; + if('text' !== sort && !this.complexItems) { + return; // need a valid sort key + } + + this.items.sort( (a, b) => { + const a1 = a[key]; + const b1 = b[key]; + if(!a1) { + return -1; + } + if(!b1) { + return 1; + } + return a1.localeCompare( b1, { sensitivity : false, numeric : true } ); + }); + + this.sorted = true; }; MenuView.prototype.removeItem = function(index) { - this.sorted = false; - this.items.splice(index, 1); + this.sorted = false; + this.items.splice(index, 1); - if (this.focusItems) { - this.focusItems.splice(index, 1); - } + if(this.focusItems) { + this.focusItems.splice(index, 1); + } - if (this.focusedItemIndex >= index) { - this.focusedItemIndex = Math.max(this.focusedItemIndex - 1, 0); - } + if(this.focusedItemIndex >= index) { + this.focusedItemIndex = Math.max(this.focusedItemIndex - 1, 0); + } - this.removeRenderCacheItem(index); + this.removeRenderCacheItem(index); - this.positionCacheExpired = true; + this.positionCacheExpired = true; }; MenuView.prototype.getCount = function() { - return this.items.length; + return this.items.length; }; MenuView.prototype.getItems = function() { - if (this.complexItems) { - return this.items; - } + if(this.complexItems) { + return this.items; + } - return this.items.map(item => { - return item.text; - }); + return this.items.map( item => { + return item.text; + }); }; MenuView.prototype.getItem = function(index) { - if (this.complexItems) { - return this.items[index]; - } + if(this.complexItems) { + return this.items[index]; + } - return this.items[index].text; + return this.items[index].text; }; MenuView.prototype.focusNext = function() { - this.emitIndexUpdate(); + this.emitIndexUpdate(); }; MenuView.prototype.focusPrevious = function() { - this.emitIndexUpdate(); + this.emitIndexUpdate(); }; MenuView.prototype.focusNextPageItem = function() { - this.emitIndexUpdate(); + this.emitIndexUpdate(); }; MenuView.prototype.focusPreviousPageItem = function() { - this.emitIndexUpdate(); + this.emitIndexUpdate(); }; MenuView.prototype.focusFirst = function() { - this.emitIndexUpdate(); + this.emitIndexUpdate(); }; MenuView.prototype.focusLast = function() { - this.emitIndexUpdate(); + this.emitIndexUpdate(); }; MenuView.prototype.setFocusItemIndex = function(index) { - this.focusedItemIndex = index; + this.focusedItemIndex = index; }; MenuView.prototype.onKeyPress = function(ch, key) { - const itemIndex = this.getHotKeyItemIndex(ch); - if (itemIndex >= 0) { - this.setFocusItemIndex(itemIndex); + const itemIndex = this.getHotKeyItemIndex(ch); + if(itemIndex >= 0) { + this.setFocusItemIndex(itemIndex); - if (true === this.hotKeySubmit) { - this.emit('action', 'accept'); + if(true === this.hotKeySubmit) { + this.emit('action', 'accept'); + } } - } - MenuView.super_.prototype.onKeyPress.call(this, ch, key); + MenuView.super_.prototype.onKeyPress.call(this, ch, key); }; MenuView.prototype.setFocusItems = function(items) { - const self = this; + const self = this; - if (items) { - this.focusItems = []; - items.forEach(itemText => { - this.focusItems.push( - { - text: self.disablePipe ? itemText : pipeToAnsi(itemText, self.client) - } - ); - }); - } + if(items) { + this.focusItems = []; + items.forEach( itemText => { + this.focusItems.push( + { + text : self.disablePipe ? itemText : pipeToAnsi(itemText, self.client) + } + ); + }); + } }; MenuView.prototype.setItemSpacing = function(itemSpacing) { - itemSpacing = parseInt(itemSpacing); - assert(_.isNumber(itemSpacing)); + itemSpacing = parseInt(itemSpacing); + assert(_.isNumber(itemSpacing)); - this.itemSpacing = itemSpacing; - this.positionCacheExpired = true; + this.itemSpacing = itemSpacing; + this.positionCacheExpired = true; }; MenuView.prototype.setItemHorizSpacing = function(itemHorizSpacing) { - itemHorizSpacing = parseInt(itemHorizSpacing); - assert(_.isNumber(itemHorizSpacing)); + itemHorizSpacing = parseInt(itemHorizSpacing); + assert(_.isNumber(itemHorizSpacing)); - this.itemHorizSpacing = itemHorizSpacing; - this.positionCacheExpired = true; + 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.setJustify(value); break; - case 'fillChar': this.setFillChar(value); break; - case 'focusItemIndex': this.focusedItemIndex = value; break; + 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.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 'itemFormat' : + case 'focusItemFormat' : + this[propName] = value; + // if there is a cache currently, invalidate it + this.invalidateRenderCache(); + break; - case 'sort': this.setSort(value); break; - } + case 'sort' : this.setSort(value); break; + } - MenuView.super_.prototype.setPropertyValue.call(this, propName, value); + MenuView.super_.prototype.setPropertyValue.call(this, propName, value); }; MenuView.prototype.setFillChar = function(fillChar) { @@ -305,18 +305,18 @@ MenuView.prototype.setJustify = function(justify) { this.justify = justify; this.invalidateRenderCache(); this.positionCacheExpired = true; -}; +} MenuView.prototype.setHotKeys = function(hotKeys) { - if (_.isObject(hotKeys)) { - if (this.caseInsensitiveHotKeys) { - this.hotKeys = {}; - for (var key in hotKeys) { - this.hotKeys[key.toLowerCase()] = hotKeys[key]; - } - } else { - this.hotKeys = hotKeys; + if(_.isObject(hotKeys)) { + if(this.caseInsensitiveHotKeys) { + this.hotKeys = {}; + for(var key in hotKeys) { + this.hotKeys[key.toLowerCase()] = hotKeys[key]; + } + } else { + this.hotKeys = hotKeys; + } } - } }; From 7735017791927dde6d3e08d6e74547903d4c654e Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Mon, 24 Jan 2022 12:35:28 -0600 Subject: [PATCH 062/146] Changed var to let/const --- core/full_menu_view.js | 46 +++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/core/full_menu_view.js b/core/full_menu_view.js index bbdda4b6..8cc61dee 100644 --- a/core/full_menu_view.js +++ b/core/full_menu_view.js @@ -59,7 +59,7 @@ function FullMenuView(options) { this.clearPage = function() { - var width = this.dimens.width; + let width = this.dimens.width; if (this.oldDimens) { if (this.oldDimens.width > width) { width = this.oldDimens.width; @@ -67,8 +67,8 @@ function FullMenuView(options) { delete this.oldDimens; } - for (var i = 0; i < this.dimens.height; i++) { - let text = `${strUtil.pad(this.fillChar, width, this.fillChar, 'left')}`; + for (let i = 0; i < this.dimens.height; i++) { + const text = `${strUtil.pad(this.fillChar, width, this.fillChar, 'left')}`; self.client.term.write(`${ansi.goto(this.position.row + i, this.position.col)}${this.getSGR()}${text}`); } } @@ -95,16 +95,16 @@ function FullMenuView(options) { this.itemsPerRow = this.items.length; } - var col = this.position.col; - var row = this.position.row; - var spacer = this.getSpacer(); + let col = this.position.col; + let row = this.position.row; + const spacer = this.getSpacer(); - var itemInRow = 0; - var itemInCol = 0; + let itemInRow = 0; + let itemInCol = 0; - var pageStart = 0; + let pageStart = 0; - for (var i = 0; i < this.items.length; ++i) { + for (let i = 0; i < this.items.length; ++i) { itemInRow++; this.items[i].row = row; this.items[i].col = col; @@ -114,19 +114,19 @@ function FullMenuView(options) { // have to calculate the max length on the last entry if (i == this.items.length - 1) { - var maxLength = 0; - for (var j = 0; j < this.itemsPerRow; j++) { + let maxLength = 0; + for (let j = 0; j < this.itemsPerRow; j++) { if (this.items[i - j].col != this.items[i].col) { break; } - var itemLength = this.items[i - j].text.length; + const itemLength = this.items[i - j].text.length; if (itemLength > maxLength) { maxLength = itemLength; } } // set length on each item in the column - for (var j = 0; j < this.itemsPerRow; j++) { + for (let j = 0; j < this.itemsPerRow; j++) { if (this.items[i - j].col != this.items[i].col) { break; } @@ -141,7 +141,7 @@ function FullMenuView(options) { this.pages.push({ start: pageStart, end: i - itemInRow }); // fix the last column processed - for (var j = 0; j < this.itemsPerRow; j++) { + for (let j = 0; j < this.itemsPerRow; j++) { if (this.items[i - j].col != col) { break; } @@ -161,17 +161,17 @@ function FullMenuView(options) { // restart row for next column row = this.position.row; - var maxLength = 0; - for (var j = 0; j < this.itemsPerRow; j++) { + let maxLength = 0; + for (let j = 0; j < this.itemsPerRow; j++) { // TODO: handle complex items - var itemLength = this.items[i - j].text.length; + let itemLength = this.items[i - j].text.length; if (itemLength > maxLength) { maxLength = itemLength; } } // set length on each item in the column - for (var j = 0; j < this.itemsPerRow; j++) { + for (let j = 0; j < this.itemsPerRow; j++) { this.items[i - j].fixedLength = maxLength; } @@ -189,7 +189,7 @@ function FullMenuView(options) { itemInRow = 0; // fix the last column processed - for (var j = 0; j < this.itemsPerRow; j++) { + for (let j = 0; j < this.itemsPerRow; j++) { this.items[i - j].col = col; } @@ -395,11 +395,11 @@ FullMenuView.prototype.focusPrevious = function() { FullMenuView.prototype.focusPreviousColumn = function() { - var currentRow = this.items[this.focusedItemIndex].itemInRow; + const currentRow = this.items[this.focusedItemIndex].itemInRow; this.focusedItemIndex = this.focusedItemIndex - this.itemsPerRow; if (this.focusedItemIndex < 0) { this.clearPage(); - var lastItemRow = this.items[this.items.length - 1].itemInRow; + const lastItemRow = this.items[this.items.length - 1].itemInRow; if (lastItemRow > currentRow) { this.focusedItemIndex = this.items.length - (lastItemRow - currentRow) - 1; } @@ -425,7 +425,7 @@ FullMenuView.prototype.focusPreviousColumn = function() { FullMenuView.prototype.focusNextColumn = function() { - var currentRow = this.items[this.focusedItemIndex].itemInRow; + const currentRow = this.items[this.focusedItemIndex].itemInRow; this.focusedItemIndex = this.focusedItemIndex + this.itemsPerRow; if (this.focusedItemIndex > this.items.length - 1) { this.focusedItemIndex = currentRow - 1; From 626329d180970f081e186ad3112c2e6816475a93 Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Mon, 24 Jan 2022 12:56:58 -0600 Subject: [PATCH 063/146] Removed self --- core/full_menu_view.js | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/core/full_menu_view.js b/core/full_menu_view.js index 8cc61dee..4ac48504 100644 --- a/core/full_menu_view.js +++ b/core/full_menu_view.js @@ -28,10 +28,6 @@ function FullMenuView(options) { this.initDefaultWidth(); - - - const self = this; - // we want page up/page down by default if (!_.isObject(options.specialKeyMap)) { Object.assign(this.specialKeyMap, { @@ -51,14 +47,7 @@ function FullMenuView(options) { this.autoAdjustHeightIfEnabled(); - - - this.getSpacer = function() { - return new Array(self.itemHorizSpacing + 1).join(this.fillChar); - } - - - this.clearPage = function() { + this.clearPage = () => { let width = this.dimens.width; if (this.oldDimens) { if (this.oldDimens.width > width) { @@ -69,11 +58,11 @@ function FullMenuView(options) { for (let i = 0; i < this.dimens.height; i++) { const text = `${strUtil.pad(this.fillChar, width, this.fillChar, 'left')}`; - self.client.term.write(`${ansi.goto(this.position.row + i, this.position.col)}${this.getSGR()}${text}`); + this.client.term.write(`${ansi.goto(this.position.row + i, this.position.col)}${this.getSGR()}${text}`); } } - this.cachePositions = function() { + this.cachePositions = () => { if (this.positionCacheExpired) { // first, clear the page this.clearPage(); @@ -97,7 +86,7 @@ function FullMenuView(options) { let col = this.position.col; let row = this.position.row; - const spacer = this.getSpacer(); + const spacer = new Array(this.itemHorizSpacing + 1).join(this.fillChar); let itemInRow = 0; let itemInCol = 0; From 113db826cf45a25f1d52493d6b7bed86e5e3b517 Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Mon, 24 Jan 2022 13:08:54 -0600 Subject: [PATCH 064/146] Changed to arrow functions --- core/full_menu_view.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/full_menu_view.js b/core/full_menu_view.js index 4ac48504..212b4d15 100644 --- a/core/full_menu_view.js +++ b/core/full_menu_view.js @@ -36,7 +36,7 @@ function FullMenuView(options) { }); } - this.autoAdjustHeightIfEnabled = function() { + 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); @@ -200,7 +200,7 @@ function FullMenuView(options) { this.positionCacheExpired = false; }; - this.drawItem = function(index) { + this.drawItem = (index) => { const item = this.items[index]; if (!item) { return; From 3b41a1837570fbf03de302c9e493815872e07b9f Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Mon, 24 Jan 2022 22:57:03 +0200 Subject: [PATCH 065/146] s/engima/enigma where the hell did I put these configs?? --- docs/installation/docker.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/installation/docker.md b/docs/installation/docker.md index 85de3aac..5510bc6c 100644 --- a/docs/installation/docker.md +++ b/docs/installation/docker.md @@ -9,17 +9,17 @@ for every operating system on the [Docker website](https://docs.docker.com/engin - Generate some config for your BBS: ``` - docker run -it -v "${HOME}/engima-bbs/config:/enigma-bbs/config" enigmabbs/enigma-bbs:latest oputil.js config new + docker run -it -v "${HOME}/enigma-bbs/config:/enigma-bbs/config" enigmabbs/enigma-bbs:latest oputil.js config new ``` - Run it: ``` - docker run -p 8888:8888 -v "${HOME}/engima-bbs/config:/enigma-bbs/config" enigmabbs/enigma-bbs:latest + docker run -p 8888:8888 -v "${HOME}/enigma-bbs/config:/enigma-bbs/config" enigmabbs/enigma-bbs:latest ``` -:bulb: Configuration will be stored in `${HOME}/engima-bbs/config`. +:bulb: Configuration will be stored in `${HOME}/enigma-bbs/config`. -:bulb: Windows users - you'll need to switch out `${HOME}/engima-bbs/config` for a Windows-style path. +:bulb: Windows users - you'll need to switch out `${HOME}/enigma-bbs/config` for a Windows-style path. ## Volumes From 7b50db6f27692d3bb04bddf8d9868e0a1c76e228 Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Tue, 25 Jan 2022 00:47:25 +0200 Subject: [PATCH 066/146] move volume creation before copying files to make them avilable on external volumes --- docker/Dockerfile | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 2ceaee30..1fe74eed 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -5,6 +5,15 @@ LABEL maintainer="dave@force9.org" ENV NVM_DIR /root/.nvm ENV DEBIAN_FRONTEND noninteractive +# 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 + COPY . /enigma-bbs # Do some installing! @@ -28,15 +37,6 @@ RUN apt-get update && apt-get install -y \ # sexyz COPY docker/bin/sexyz /usr/local/bin -# 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 From bcef8b3d3e66898d47ef79fcccda235dacb78149 Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Tue, 25 Jan 2022 01:08:35 +0200 Subject: [PATCH 067/146] =?UTF-8?q?node-pre-gyp@0.11.0=20requires=20python?= =?UTF-8?q?3=20to=20build=20now=20=C2=AF\=5F(=E3=83=84)=5F/=C2=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/Dockerfile b/docker/Dockerfile index 1fe74eed..c55869e9 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -21,6 +21,7 @@ RUN apt-get update && apt-get install -y \ git \ curl \ build-essential \ + python3 \ python \ libssl-dev \ lrzsz \ From 39c06b71eca1b5dab1ebe6e4ec9a8c30ee742eb9 Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Mon, 24 Jan 2022 17:10:59 -0600 Subject: [PATCH 068/146] Fixed links and md in full_menu_view --- docs/art/views/full_menu_view.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/docs/art/views/full_menu_view.md b/docs/art/views/full_menu_view.md index 03db2cbb..a513117e 100644 --- a/docs/art/views/full_menu_view.md +++ b/docs/art/views/full_menu_view.md @@ -11,14 +11,14 @@ Items can be selected on a menu via the cursor keys, Page Up, Page Down, Home, a :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 [Art](../art.md) for general information on how to use views and common configuration properties available for them. +:information_source: See [Art](../general.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 [Art](../art.md) | -| `focusTextStyle` | Sets focus text style. See **Text Styles** in [Art](../art.md)| +| `textStyle` | Sets the standard (non-focus) text style. See **Text Styles** in [Art](../general.md) | +| `focusTextStyle` | Sets focus text style. See **Text Styles** in [Art](../general.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) | @@ -29,11 +29,11 @@ Items can be selected on a menu via the cursor keys, Page Up, Page Down, Home, a | `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 [Art](../art.md) | +| `itemFormat` | Sets the format for a list entry. See **Entry Formatting** in [Art](../general.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 [Art](../art.md) | +| `focusItemFormat` | Sets the format for a focused list entry. See **Entry Formatting** in [Art](../general.md) | ### Hot Keys @@ -84,7 +84,7 @@ The `textOverflow` option is used to specify what happens when a text string is
Configuration fragment (expand to view) - +
``` FM1: { submit: true @@ -111,6 +111,7 @@ FM1: { } ``` +
### A simple horizontal menu - similar to HM @@ -119,7 +120,7 @@ FM1: {
Configuration fragment (expand to view) - +
``` FM2: { focus: true @@ -132,6 +133,7 @@ FM2: { ] } ``` +
### A multi-column navigation menu with hotkeys @@ -141,7 +143,7 @@ FM2: {
Configuration fragment (expand to view) - +
``` FM1: { focus: true @@ -227,5 +229,6 @@ FM1: { ] } ``` +
From 234eef7585a82c03bb0faebcd156d70a37881891 Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Tue, 25 Jan 2022 12:12:05 +0200 Subject: [PATCH 069/146] must remain in v12 for sqlite to build correctly according to lock --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index c55869e9..8448ef17 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM node:lts-buster-slim +FROM node:12-buster-slim LABEL maintainer="dave@force9.org" From 84adb9fe59d6adf7816336ec6ba2f8bb4326cf1f Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Tue, 25 Jan 2022 12:28:58 -0600 Subject: [PATCH 070/146] Added documentation for other menu views --- docs/art/views/horizontal_menu_view.md | 91 +++++++++++++++ docs/art/views/spinner_menu_view.md | 98 ++++++++++++++++ docs/art/views/vertical_menu_view.md | 106 ++++++++++++++++++ .../images/horizontal_menu_view_example1.gif | Bin 0 -> 7832 bytes .../images/spinner_menu_view_example1.gif | Bin 0 -> 3982 bytes .../images/vertical_menu_view_example1.gif | Bin 0 -> 7198 bytes 6 files changed, 295 insertions(+) create mode 100644 docs/art/views/horizontal_menu_view.md create mode 100644 docs/art/views/spinner_menu_view.md create mode 100644 docs/art/views/vertical_menu_view.md create mode 100644 docs/assets/images/horizontal_menu_view_example1.gif create mode 100644 docs/assets/images/spinner_menu_view_example1.gif create mode 100644 docs/assets/images/vertical_menu_view_example1.gif diff --git a/docs/art/views/horizontal_menu_view.md b/docs/art/views/horizontal_menu_view.md new file mode 100644 index 00000000..d0b13144 --- /dev/null +++ b/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 [Art](../general.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 [Art](../general.md) | +| `focusTextStyle` | Sets focus text style. See **Text Styles** in [Art](../general.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 [Art](../general.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 [Art](../general.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/art/views/spinner_menu_view.md b/docs/art/views/spinner_menu_view.md new file mode 100644 index 00000000..81acb6ed --- /dev/null +++ b/docs/art/views/spinner_menu_view.md @@ -0,0 +1,98 @@ +--- +layout: page +title: Spinner Menu View +--- +## Spinner Menu View +A spinner menu view supports displaying a list 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 [Art](../general.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 [Art](../general.md) | +| `focusTextStyle` | Sets focus text style. See **Text Styles** in [Art](../general.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 [Art](../general.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 [Art](../general.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 + } +] +``` + +## 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/art/views/vertical_menu_view.md b/docs/art/views/vertical_menu_view.md new file mode 100644 index 00000000..61654746 --- /dev/null +++ b/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 [Art](../general.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 [Art](../general.md) | +| `focusTextStyle` | Sets focus text style. See **Text Styles** in [Art](../general.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 [Art](../general.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 [Art](../general.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/assets/images/horizontal_menu_view_example1.gif b/docs/assets/images/horizontal_menu_view_example1.gif new file mode 100644 index 0000000000000000000000000000000000000000..e02c60a44af7e108ac7c5834030d7bdccb0b4863 GIT binary patch literal 7832 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 literal 0 HcmV?d00001 diff --git a/docs/assets/images/spinner_menu_view_example1.gif b/docs/assets/images/spinner_menu_view_example1.gif new file mode 100644 index 0000000000000000000000000000000000000000..8b7fb1a86030ce7706a51e1dfb578f355b7cc938 GIT binary patch literal 3982 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 B#RdQX literal 0 HcmV?d00001 diff --git a/docs/assets/images/vertical_menu_view_example1.gif b/docs/assets/images/vertical_menu_view_example1.gif new file mode 100644 index 0000000000000000000000000000000000000000..7366d8130dbada70680437908d58f1ad98b2c0aa GIT binary patch literal 7198 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 qQn Date: Tue, 25 Jan 2022 12:37:40 -0600 Subject: [PATCH 071/146] Minor changes on spinner and full menu --- docs/art/views/full_menu_view.md | 6 ++++++ docs/art/views/spinner_menu_view.md | 10 ++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/art/views/full_menu_view.md b/docs/art/views/full_menu_view.md index a513117e..55f9b4cc 100644 --- a/docs/art/views/full_menu_view.md +++ b/docs/art/views/full_menu_view.md @@ -66,6 +66,12 @@ items: [ ] ``` +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. diff --git a/docs/art/views/spinner_menu_view.md b/docs/art/views/spinner_menu_view.md index 81acb6ed..42c88e14 100644 --- a/docs/art/views/spinner_menu_view.md +++ b/docs/art/views/spinner_menu_view.md @@ -3,7 +3,7 @@ layout: page title: Spinner Menu View --- ## Spinner Menu View -A spinner menu view supports displaying a list 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. +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 @@ -46,7 +46,7 @@ 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: +A spinner menu, similar to other menus, take a list of items to display in the menu. For example: ``` @@ -62,6 +62,12 @@ items: [ ] ``` +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") From ac13a437c34483c50dc3c8484b7727dad2f1e8b8 Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Tue, 25 Jan 2022 12:41:20 -0600 Subject: [PATCH 072/146] Added menu views to navigation --- docs/_includes/nav.md | 3 +++ docs/art/mci.md | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md index cd635928..a0788ddc 100644 --- a/docs/_includes/nav.md +++ b/docs/_includes/nav.md @@ -51,6 +51,9 @@ - [MCI Codes]({{ site.baseurl }}{% link art/mci.md %}) - Views - [Full Menu]({{ site.baseurl }}{% link art/views/full_menu_view.md %}) + - [Horizontal Menu]({{ site.baseurl }}{% link art/views/horizontal_menu_view.md %}) + - [Spinner Menu]({{ site.baseurl }}{% link art/views/spinner_menu_view.md %}) + - [Vertical Menu]({{ site.baseurl }}{% link art/views/vertical_menu_view.md %}) - Servers - Login Servers diff --git a/docs/art/mci.md b/docs/art/mci.md index 79135554..2e218200 100644 --- a/docs/art/mci.md +++ b/docs/art/mci.md @@ -121,10 +121,10 @@ a Vertical Menu (`%VM`): Old-school BBSers may recognize this as a lightbar menu | `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 | +| `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 | +| `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 | | `KE` | Key Entry | A *single* key input control | Think hotkeys | From 16eceab05b010835f7677c87a225bbf625397b00 Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Tue, 25 Jan 2022 12:44:15 -0600 Subject: [PATCH 073/146] Tweak to full menu listing --- docs/art/mci.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/art/mci.md b/docs/art/mci.md index 2e218200..bdd69056 100644 --- a/docs/art/mci.md +++ b/docs/art/mci.md @@ -123,7 +123,7 @@ a Vertical Menu (`%VM`): Old-school BBSers may recognize this as a lightbar menu | `BT` | Button | A button | ...it's a button | | `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) | +| `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 | | `KE` | Key Entry | A *single* key input control | Think hotkeys | From b791c9e9bb8c31e1ab8864045035c18ff4cdb601 Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Tue, 25 Jan 2022 23:30:38 +0200 Subject: [PATCH 074/146] this looks like it does the trick - first time will issue a config procedure (I know we have redundant files in enigma-bbs-pre but that's a price for the enhanced deployment ease) --- docker/Dockerfile | 45 ++++++++++++++++++++------------- docker/bin/docker-entrypoint.sh | 16 ++++++++++++ docs/installation/docker.md | 43 ++++++++++++++++++++++--------- 3 files changed, 74 insertions(+), 30 deletions(-) create mode 100644 docker/bin/docker-entrypoint.sh diff --git a/docker/Dockerfile b/docker/Dockerfile index 8448ef17..1784e250 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -4,20 +4,12 @@ LABEL maintainer="dave@force9.org" ENV NVM_DIR /root/.nvm ENV DEBIAN_FRONTEND noninteractive - -# 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 - COPY . /enigma-bbs -# Do some installing! -RUN apt-get update && apt-get install -y \ +# 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 \ @@ -29,18 +21,35 @@ RUN apt-get update && apt-get install -y \ lhasa \ unrar-free \ p7zip-full \ - && npm install -g pm2 \ - && cd /enigma-bbs && npm install --only=production \ - && apt-get remove build-essential python libssl-dev git curl -y && apt-get autoremove -y \ - && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \ - && apt-get clean + && 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 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 +# 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 -CMD ["pm2-runtime", "main.js"] +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..1e0927d2 --- /dev/null +++ b/docker/bin/docker-entrypoint.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +prepopvols=("config" "mods" "art") +bbspath=/enigma-bbs +bbsstgp=/enigma-bbs-pre +if [[ ! -f $bbspath/config/config.hjson ]]; then + for dir in "${prepopvols[@]}" + do + if [ -n "$(find "$bbspath/$dir" -maxdepth 0 -type d -empty 2>/dev/null)" ]; then + cp -rp $bbsstgp/$dir/* $bbspath/$dir/ + else + echo "WARN skipped $bbspath/$dir - vol Not empty/not a new setup - possible bad state" + fi + done + ./oputil.js config new +fi +pm2-runtime main.js \ No newline at end of file diff --git a/docs/installation/docker.md b/docs/installation/docker.md index 5510bc6c..39ce70df 100644 --- a/docs/installation/docker.md +++ b/docs/installation/docker.md @@ -6,20 +6,39 @@ title: Docker 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 --rm -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: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 --rm -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:latest +``` -- Generate some config for your BBS: - ``` - docker run -it -v "${HOME}/enigma-bbs/config:/enigma-bbs/config" enigmabbs/enigma-bbs:latest oputil.js config new - ``` +:bulb: Configuration will be stored in `$(pwd)/enigma-bbs/config`. -- Run it: - ``` - docker run -p 8888:8888 -v "${HOME}/enigma-bbs/config:/enigma-bbs/config" enigmabbs/enigma-bbs:latest - ``` - -:bulb: Configuration will be stored in `${HOME}/enigma-bbs/config`. - -:bulb: Windows users - you'll need to switch out `${HOME}/enigma-bbs/config` for a Windows-style path. +:bulb: Windows users - you'll need to switch out `$(pwd)/enigma-bbs/config` for a Windows-style path. ## Volumes From d63db9acc27c2428a603e7b4e0ab387918e23896 Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Wed, 26 Jan 2022 00:44:39 +0200 Subject: [PATCH 075/146] removed python3 - not needed when running 12-buster-slim also prep entrypoint even if it was shipped with no execution attrib and prevent restart loop since pm2-runtime always exits with 0 --- docker/Dockerfile | 2 +- docker/bin/docker-entrypoint.sh | 20 +++++++++++++++----- docs/installation/docker.md | 6 +++--- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 1784e250..f143d0d6 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -13,7 +13,6 @@ RUN apt-get update \ git \ curl \ build-essential \ - python3 \ python \ libssl-dev \ lrzsz \ @@ -37,6 +36,7 @@ RUN apt-get update \ # 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 diff --git a/docker/bin/docker-entrypoint.sh b/docker/bin/docker-entrypoint.sh index 1e0927d2..1a6c6e3d 100644 --- a/docker/bin/docker-entrypoint.sh +++ b/docker/bin/docker-entrypoint.sh @@ -1,8 +1,13 @@ #!/usr/bin/env bash -prepopvols=("config" "mods" "art") -bbspath=/enigma-bbs -bbsstgp=/enigma-bbs-pre -if [[ ! -f $bbspath/config/config.hjson ]]; then + +# Set some vars +prepopvols=("config" "mods" "art") # these are folders which contain runtime needed files, and need to be represented in the host +bbspath=/enigma-bbs # install location +bbsstgp=/enigma-bbs-pre # staging location for prepopvals +configname=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 $bbspath/config/$configname ]]; then for dir in "${prepopvols[@]}" do if [ -n "$(find "$bbspath/$dir" -maxdepth 0 -type d -empty 2>/dev/null)" ]; then @@ -13,4 +18,9 @@ if [[ ! -f $bbspath/config/config.hjson ]]; then done ./oputil.js config new fi -pm2-runtime main.js \ No newline at end of file +if [[ ! -f $bbspath/config/$configname ]]; then #make sure once more, otherwise pm2-runtime will loop if missing the config + echo "for some reason you have skipped configuration - enigma will not work. please run config" + exit 1 +else + pm2-runtime main.js +fi diff --git a/docs/installation/docker.md b/docs/installation/docker.md index 39ce70df..13cf0985 100644 --- a/docs/installation/docker.md +++ b/docs/installation/docker.md @@ -19,7 +19,7 @@ docker run -it --rm -p 8888:8888 \ -v "$(pwd)/art:/enigma-bbs/art" \ -v "$(pwd)/mods:/enigma-bbs/mods" \ -v "$(pwd)/mail:/mail" \ -enigmabbs:latest +enigmabbs/enigmabbs: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) @@ -33,8 +33,8 @@ docker run -d --rm -p 8888:8888 \ -v "$(pwd)/art:/enigma-bbs/art" \ -v "$(pwd)/mods:/enigma-bbs/mods" \ -v "$(pwd)/mail:/mail" \ -enigmabbs:latest -``` +enigmabbs/enigmabbs:latest +```` :bulb: Configuration will be stored in `$(pwd)/enigma-bbs/config`. From 48893576151de68cffe58d5c30c571fa565446bf Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Wed, 26 Jan 2022 01:39:07 +0200 Subject: [PATCH 076/146] no need to --rm unless there's issues really. (or upgrade ---- tbd) --- docs/installation/docker.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation/docker.md b/docs/installation/docker.md index 13cf0985..f2255efb 100644 --- a/docs/installation/docker.md +++ b/docs/installation/docker.md @@ -10,7 +10,7 @@ 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 --rm -p 8888:8888 \ +docker run -it -p 8888:8888 \ --name "EnigmaBBS" \ -v "$(pwd)/config:/enigma-bbs/config" \ -v "$(pwd)/db:/enigma-bbs/db" \ @@ -24,7 +24,7 @@ enigmabbs/enigmabbs: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 --rm -p 8888:8888 \ +docker run -d -p 8888:8888 \ --name "EnigmaBBS" \ -v "$(pwd)/config:/enigma-bbs/config" \ -v "$(pwd)/db:/enigma-bbs/db" \ From 4957d7ea0d50bb44444ea6b38c01caed79726ad3 Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Wed, 26 Jan 2022 01:43:11 +0200 Subject: [PATCH 077/146] no need to --rm unless there's issues really. (or upgrade ---- tbd) --- docs/installation/docker.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/installation/docker.md b/docs/installation/docker.md index f2255efb..4b618ddb 100644 --- a/docs/installation/docker.md +++ b/docs/installation/docker.md @@ -35,6 +35,10 @@ docker run -d -p 8888:8888 \ -v "$(pwd)/mail:/mail" \ enigmabbs/enigmabbs: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`. From 0f6db152f38153769258ebeafaedb84077b237ab Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Wed, 26 Jan 2022 00:44:39 +0200 Subject: [PATCH 078/146] python3 needed after all. --- docker/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/Dockerfile b/docker/Dockerfile index f143d0d6..3271f088 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -14,6 +14,7 @@ RUN apt-get update \ curl \ build-essential \ python \ + python 3 \ libssl-dev \ lrzsz \ arj \ From c3b7d897653ffcd15f50795b25ded27bc276158d Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Wed, 26 Jan 2022 12:13:41 +0200 Subject: [PATCH 079/146] clean up after py3 --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 3271f088..c9ac8135 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -30,7 +30,7 @@ RUN apt-get update \ && 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 libssl-dev git curl -y \ + && 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 From 0240a130434b0563fdea8f9294ee93fc93cc9785 Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Wed, 26 Jan 2022 13:08:58 +0200 Subject: [PATCH 080/146] clean up after py3 --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index c9ac8135..aa8047b0 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -14,7 +14,7 @@ RUN apt-get update \ curl \ build-essential \ python \ - python 3 \ + python3 \ libssl-dev \ lrzsz \ arj \ From 8f56b7e48c9c2875c77932ac4f653978f52e3bef Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Wed, 26 Jan 2022 13:10:03 +0200 Subject: [PATCH 081/146] indentation --- docker/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index aa8047b0..1e5b1711 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,9 +22,9 @@ RUN apt-get update \ unrar-free \ p7zip-full \ && npm install -g pm2 \ - && cd /enigma-bbs && npm install --only=production \ + && cd /enigma-bbs && npm install --only=production \ && pm2 start main.js \ - && mkdir -p /enigma-bbs-pre/art \ + && mkdir -p /enigma-bbs-pre/art \ && mkdir /enigma-bbs-pre/mods \ && mkdir /enigma-bbs-pre/config \ && cp -rp art/* ../enigma-bbs-pre/art/ \ From 742418daebd1274358a90387c35402a3d809cfab Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Wed, 26 Jan 2022 16:21:40 -0600 Subject: [PATCH 082/146] Added BT, PL, and TL --- docs/art/views/button_view.md | 57 ++++++++++++++++++ docs/art/views/predefined_label_view.md | 49 +++++++++++++++ docs/art/views/text_view.md | 49 +++++++++++++++ docs/assets/images/button_view_example1.gif | Bin 0 -> 5255 bytes .../images/predefined_label_view_example1.png | Bin 0 -> 3342 bytes .../images/text_label_view_example1.png | Bin 0 -> 1479 bytes 6 files changed, 155 insertions(+) create mode 100644 docs/art/views/button_view.md create mode 100644 docs/art/views/predefined_label_view.md create mode 100644 docs/art/views/text_view.md create mode 100644 docs/assets/images/button_view_example1.gif create mode 100644 docs/assets/images/predefined_label_view_example1.png create mode 100644 docs/assets/images/text_label_view_example1.png diff --git a/docs/art/views/button_view.md b/docs/art/views/button_view.md new file mode 100644 index 00000000..23f52229 --- /dev/null +++ b/docs/art/views/button_view.md @@ -0,0 +1,57 @@ +--- +layout: page +title: Full Menu View +--- +## Full Menu 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 [Art](../general.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 [Art](../general.md) | +| `focusTextStyle` | Sets focus text style. See **Text Styles** in [Art](../general.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 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 | + +### 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/art/views/predefined_label_view.md b/docs/art/views/predefined_label_view.md new file mode 100644 index 00000000..45d2d1cd --- /dev/null +++ b/docs/art/views/predefined_label_view.md @@ -0,0 +1,49 @@ +--- +layout: page +title: Full Menu View +--- +## Full Menu 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 [Art](../general.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 [Art](../general.md) | +| `justify` | Sets the justification of each item in the list. Options: left (default), right, center | +| `fillChar` | Specifies a character to fill extra space in the menu with. Defaults to an empty space | +| `width` | Specifies the width that the value should be displayed in (default 3) | +| `textOverflow` | If a single column 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 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/art/views/text_view.md b/docs/art/views/text_view.md new file mode 100644 index 00000000..ad2e0855 --- /dev/null +++ b/docs/art/views/text_view.md @@ -0,0 +1,49 @@ +--- +layout: page +title: Full Menu View +--- +## Full Menu 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 [Art](../general.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 [Art](../general.md) | +| `width` | Sets the width of a view to display one or more columns horizontally (default 15)| +| `argName` | Sets the argument name for this selection in the form - *Not normally used for text labels* | +| `justify` | Sets the justification of each item in the list. Options: left (default), right, center | +| `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 | + +### 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/assets/images/button_view_example1.gif b/docs/assets/images/button_view_example1.gif new file mode 100644 index 0000000000000000000000000000000000000000..c7e36d02faa31f4a053121c4a8edf9a6337fbd72 GIT binary patch literal 5255 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 u%~0NK(+hh;bjmE(3&P&wjWt9l4$11P7 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 literal 0 HcmV?d00001 From 19a708c2483d71483836720179d2eff6a638fa9e Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Wed, 26 Jan 2022 17:22:13 -0600 Subject: [PATCH 083/146] Added ME and ET, additional cleanup --- docs/art/views/button_view.md | 4 +- docs/art/views/edit_text_view.md | 41 ++++++++++++++++++ docs/art/views/mask_edit_text_view.md | 40 +++++++++++++++++ docs/art/views/predefined_label_view.md | 10 ++--- docs/art/views/text_view.md | 15 +++---- .../assets/images/edit_text_view_example1.gif | Bin 0 -> 4346 bytes .../images/mask_edit_text_view_example1.gif | Bin 0 -> 7117 bytes 7 files changed, 95 insertions(+), 15 deletions(-) create mode 100644 docs/art/views/edit_text_view.md create mode 100644 docs/art/views/mask_edit_text_view.md create mode 100644 docs/assets/images/edit_text_view_example1.gif create mode 100644 docs/assets/images/mask_edit_text_view_example1.gif diff --git a/docs/art/views/button_view.md b/docs/art/views/button_view.md index 23f52229..b9b9a128 100644 --- a/docs/art/views/button_view.md +++ b/docs/art/views/button_view.md @@ -23,8 +23,8 @@ A button view supports displaying a button on a screen. | `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 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 | +| `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 diff --git a/docs/art/views/edit_text_view.md b/docs/art/views/edit_text_view.md new file mode 100644 index 00000000..e8eb779d --- /dev/null +++ b/docs/art/views/edit_text_view.md @@ -0,0 +1,41 @@ +--- +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 [Art](../general.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 [Art](../general.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.png "Text label") + +
+Configuration fragment (expand to view) +
+``` +ET1: { + maxLength: @config:users.usernameMax + argName: username + focus: true +} +``` +
+
diff --git a/docs/art/views/mask_edit_text_view.md b/docs/art/views/mask_edit_text_view.md new file mode 100644 index 00000000..da1ca06e --- /dev/null +++ b/docs/art/views/mask_edit_text_view.md @@ -0,0 +1,40 @@ +--- +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 defined by the module. Unlike a edit text view, the mask edit text view does not show the current value until the field is focused. + +## 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 [Art](../general.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 [Art](../general.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/mask_edit_text_view_example1.gif "Masked Text Edit View") + +
+Configuration fragment (expand to view) +
+``` +ME1: { + maxLength: @config:users.webMax + argName: web +} +``` +
+
diff --git a/docs/art/views/predefined_label_view.md b/docs/art/views/predefined_label_view.md index 45d2d1cd..c17275f6 100644 --- a/docs/art/views/predefined_label_view.md +++ b/docs/art/views/predefined_label_view.md @@ -1,8 +1,8 @@ --- layout: page -title: Full Menu View +title: Predefined Label View --- -## Full Menu View +## Predefined Label View A predefined label view supports displaying a predefined MCI label on a screen. ## General Information @@ -18,10 +18,10 @@ A predefined label view supports displaying a predefined MCI label on a screen. | Property | Description | |-------------|--------------| | `textStyle` | Sets the standard (non-focus) text style. See **Text Styles** in [Art](../general.md) | -| `justify` | Sets the justification of each item in the list. Options: left (default), right, center | -| `fillChar` | Specifies a character to fill extra space in the menu with. Defaults to an empty space | +| `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 a single column cannot be displayed due to `width`, set overflow characters. See **Text Overflow** below | +| `textOverflow` | If the MCI is wider than width, set overflow characters. See **Text Overflow** below | ### Text Overflow diff --git a/docs/art/views/text_view.md b/docs/art/views/text_view.md index ad2e0855..6f7f36b8 100644 --- a/docs/art/views/text_view.md +++ b/docs/art/views/text_view.md @@ -1,8 +1,8 @@ --- layout: page -title: Full Menu View +title: Text View --- -## Full Menu View +## Text View A text label view supports displaying simple text on a screen. ## General Information @@ -15,13 +15,12 @@ A text label view supports displaying simple text on a screen. | Property | Description | |-------------|--------------| -| `text` | Sets the text to display on the button | +| `text` | Sets the text to display on the label | | `textStyle` | Sets the standard (non-focus) text style. See **Text Styles** in [Art](../general.md) | -| `width` | Sets the width of a view to display one or more columns horizontally (default 15)| -| `argName` | Sets the argument name for this selection in the form - *Not normally used for text labels* | -| `justify` | Sets the justification of each item in the list. Options: left (default), right, center | -| `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 | +| `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 diff --git a/docs/assets/images/edit_text_view_example1.gif b/docs/assets/images/edit_text_view_example1.gif new file mode 100644 index 0000000000000000000000000000000000000000..1917d297214ff0a9cf969288000ed5b8b2f4b0d2 GIT binary patch literal 4346 zcmeH}YfzI{8plr(E(r-GG75?;4!8xRilr(zQQsht z2oz#M5D_o|qY<^-1JkZbATA+Lbx|uW;9{xR^$O^))LF-Uad)9#n6Yc^_JilsJ9Fke z=ehiU|L3d`MsfqT+9Eu_KM=kEUatTGfj}aWC=|-d%F5c>+Q!Dl*47q{Mq@A-EEbEy z;T#+s@OV6dKp>GwWHQ;w$%#UtP^nZJjkb94VrOS(7Z(>-S64STH#(ipU@+X>-B~P_ zhlhu!r>B>fm$$dKkB<+B!}0a?UAb~4m&*+Z2nY-e3=R$s2?<%fdNq&7b7xjo*6!WA_w3oTckkYu zoE(WnvVZ^ng9i^n5G0jKb8~a^^YaS|3S=^wTrNL+_;68CQE_o`Nl8g*Y3Y$8N0dsX zN~J0*D^si0<>lp-m6cUhRn^tiH8nL_t+uwdwyv&Dr_<^6`uh6%^XJbuG&D3dH8nRk zx3;#nwY3=xhK`Po3l}b2ym--QG@48%v)OF1So-?6l)trm z(0YJCI|~{$#zSZ#JHgzj?Uv!0qAEdC?d2lMiu7S~Q(aFfonPoIY(CkmVy&-F=xx^Z z9pxljs)Q}4zB$g#dNADEa{B6t&_i}EqE`K1>I5qG7E9}y>!)MtMAf3U`kQCtTGB@> zZD(&aBy|_ML>bQAZb`jfzopM`eyBZj%u*fI{^iKm+0Pz~^tCtq?Gl8-x~}bLyxT1& zE>G<5Xd3HLFr!XU@$>N7AXqTNFwbZ!0FSk|*$4nsfCDJ_C=&38sDeg1MQUT8sOf6d z^4SQ)+Z_E1M!Dco^Wo#>+lWH`_QOU4CqRO$UI6(2qzV*C3IMAwykcvgR<_wtmJ^qv zy`1OZz(`0MkGWj5XazeZ8I@v|Axf6|V*JLe{CCDB_Rnxm_2*NmtxY*ZriISm$0m7@ z+%0*YAE7^_L9(9XHr^?J%4FDFKE5&S-BI9!A7e__kZXaw(?%mhwuK zvJ%V+Eg@L8WcoI7odqhOkZE$t8orgkvQ< z-@qyR%?2-u7grc_EUMjG1^wj3vY-d-0{?|64R%BP;E1iLzSE$g$BoyukFN@6j}dl4unRw6gaFBDVguj>}0ix8=Gwa-463=i$l%iK4Ar2e*2F>uPY1sxT;B-u1@lnNi8~9R z&^R!D^~9{Wao4_Abm!x33wF(LzU!i949=WMb*!b9xR4J7kp`G%C2CDADd#htMUDPL z7l}pu@DSE%fNRP@Z2O=vR9Lj2KlpsNwS9p^X;{;S;6@IPl5^j7YA|2g#Z8m literal 0 HcmV?d00001 diff --git a/docs/assets/images/mask_edit_text_view_example1.gif b/docs/assets/images/mask_edit_text_view_example1.gif new file mode 100644 index 0000000000000000000000000000000000000000..443d2868de13424a671d70e892d89da35a8fe789 GIT binary patch literal 7117 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 x$kW3TQTIt#p=lKTS+|?fRwRz~{Az Date: Wed, 26 Jan 2022 17:27:38 -0600 Subject: [PATCH 084/146] Fixed image for edit text view --- docs/art/views/edit_text_view.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/art/views/edit_text_view.md b/docs/art/views/edit_text_view.md index e8eb779d..d8f37171 100644 --- a/docs/art/views/edit_text_view.md +++ b/docs/art/views/edit_text_view.md @@ -25,7 +25,7 @@ An edit text view supports editing form values on a screen. This can be for new ## Example -![Example](../../assets/images/edit_text_view_example1.png "Text label") +![Example](../../assets/images/edit_text_view_example1.gif "Edit Text View")
Configuration fragment (expand to view) From 10c9ea95541f9d12e32f4feb947289e35d714919 Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Fri, 28 Jan 2022 10:33:04 -0600 Subject: [PATCH 085/146] General bug fixes --- core/button_view.js | 13 ++----------- core/fse.js | 24 +++++++++++++++--------- core/text_view.js | 45 +-------------------------------------------- core/view.js | 2 +- 4 files changed, 19 insertions(+), 65 deletions(-) diff --git a/core/button_view.js b/core/button_view.js index edb32e12..a4f70744 100644 --- a/core/button_view.js +++ b/core/button_view.js @@ -21,7 +21,8 @@ function ButtonView(options) { util.inherits(ButtonView, TextView); ButtonView.prototype.onKeyPress = function(ch, key) { - if(this.isKeyMapped('accept', key.name) || ' ' === ch) { + let actionForKeyName = key ? key.name : ch; + if(this.isKeyMapped('accept', actionForKeyName) || ' ' === ch) { this.submitData = 'accept'; this.emit('action', 'accept'); delete this.submitData; @@ -29,16 +30,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/fse.js b/core/fse.js index 9fa0e97b..e66d40fe 100644 --- a/core/fse.js +++ b/core/fse.js @@ -791,10 +791,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 !== undefined) { + from.acceptsFocus = false; + } // :TODO: make this a method var body = self.viewControllers.body.getView(MciViewIds.body.message); @@ -825,10 +825,13 @@ 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) { @@ -914,7 +917,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 (self.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/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/view.js b/core/view.js index c31ccca1..fc62bb53 100644 --- a/core/view.js +++ b/core/view.js @@ -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; }; From 049c3a870aa580f8cce29ace1c9ffc447440cc4a Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Fri, 28 Jan 2022 11:26:27 -0600 Subject: [PATCH 086/146] Fixed backspace when first char is pattern char but next is literal --- core/mask_edit_text_view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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)); From 823fc14154a2fd673ecde9e5fc44c6c63e2a362a Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Fri, 28 Jan 2022 16:34:52 -0600 Subject: [PATCH 087/146] Additional fixes to docs --- docs/art/views/button_view.md | 6 +-- docs/art/views/edit_text_view.md | 5 ++- docs/art/views/full_menu_view.md | 10 ++--- docs/art/views/horizontal_menu_view.md | 10 ++--- docs/art/views/mask_edit_text_view.md | 40 ++++++++++++++---- docs/art/views/predefined_label_view.md | 4 +- docs/art/views/spinner_menu_view.md | 10 ++--- docs/art/views/text_view.md | 4 +- docs/art/views/vertical_menu_view.md | 10 ++--- .../images/mask_edit_text_view_example1.gif | Bin 7117 -> 7222 bytes 10 files changed, 62 insertions(+), 37 deletions(-) diff --git a/docs/art/views/button_view.md b/docs/art/views/button_view.md index b9b9a128..97dc97e8 100644 --- a/docs/art/views/button_view.md +++ b/docs/art/views/button_view.md @@ -9,15 +9,15 @@ A button view supports displaying a button on a screen. :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 [Art](../general.md) for general information on how to use views and common configuration properties available for them. +: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 [Art](../general.md) | -| `focusTextStyle` | Sets focus text style. See **Text Styles** in [Art](../general.md)| +| `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** | diff --git a/docs/art/views/edit_text_view.md b/docs/art/views/edit_text_view.md index d8f37171..c372246d 100644 --- a/docs/art/views/edit_text_view.md +++ b/docs/art/views/edit_text_view.md @@ -9,13 +9,14 @@ An edit text view supports editing form values on a screen. This can be for new :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 [Art](../general.md) for general information on how to use views and common configuration properties available for them. +: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 [Art](../general.md) | +| `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 | diff --git a/docs/art/views/full_menu_view.md b/docs/art/views/full_menu_view.md index 55f9b4cc..19ff365a 100644 --- a/docs/art/views/full_menu_view.md +++ b/docs/art/views/full_menu_view.md @@ -11,14 +11,14 @@ Items can be selected on a menu via the cursor keys, Page Up, Page Down, Home, a :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 [Art](../general.md) for general information on how to use views and common configuration properties available for them. +: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 [Art](../general.md) | -| `focusTextStyle` | Sets focus text style. See **Text Styles** in [Art](../general.md)| +| `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) | @@ -29,11 +29,11 @@ Items can be selected on a menu via the cursor keys, Page Up, Page Down, Home, a | `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 [Art](../general.md) | +| `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 [Art](../general.md) | +| `focusItemFormat` | Sets the format for a focused list entry. See **Entry Formatting** in [MCI](../mci.md) | ### Hot Keys diff --git a/docs/art/views/horizontal_menu_view.md b/docs/art/views/horizontal_menu_view.md index d0b13144..90dc4438 100644 --- a/docs/art/views/horizontal_menu_view.md +++ b/docs/art/views/horizontal_menu_view.md @@ -11,14 +11,14 @@ Items can be selected on a menu via the cursor keys, Page Up, Page Down, Home, a :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 [Art](../general.md) for general information on how to use views and common configuration properties available for them. +: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 [Art](../general.md) | -| `focusTextStyle` | Sets focus text style. See **Text Styles** in [Art](../general.md)| +| `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 | @@ -27,10 +27,10 @@ Items can be selected on a menu via the cursor keys, Page Up, Page Down, Home, a | `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 [Art](../general.md) | +| `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 [Art](../general.md) | +| `focusItemFormat` | Sets the format for a focused list entry. See **Entry Formatting** in [MCI](../mci.md) | ### Hot Keys diff --git a/docs/art/views/mask_edit_text_view.md b/docs/art/views/mask_edit_text_view.md index da1ca06e..a03e83c5 100644 --- a/docs/art/views/mask_edit_text_view.md +++ b/docs/art/views/mask_edit_text_view.md @@ -3,26 +3,49 @@ 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 defined by the module. Unlike a edit text view, the mask edit text view does not show the current value until the field is focused. +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 [Art](../general.md) for general information on how to use views and common configuration properties available for them. +: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 [Art](../general.md) | -| `width` | Sets the width of a view for the text edit (default 15)| +| `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 | +| `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 | -| `justify` | Sets the justification of the text entry. Options: left (default), right, center | +| `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") @@ -32,8 +55,9 @@ A mask edit text view supports editing form values on a screen. This can be for
``` ME1: { - maxLength: @config:users.webMax - argName: web + argName: height + fillChar: "#" + maskPattern: "# ft. ## in." } ```
diff --git a/docs/art/views/predefined_label_view.md b/docs/art/views/predefined_label_view.md index c17275f6..cae23f55 100644 --- a/docs/art/views/predefined_label_view.md +++ b/docs/art/views/predefined_label_view.md @@ -11,13 +11,13 @@ A predefined label view supports displaying a predefined MCI label on a screen. :information_source: See *Predefined Codes* in [MCI](../mci.md) for the list of available MCI codes. -:information_source: See [Art](../general.md) for general information on how to use views and common configuration properties available for them. +: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 [Art](../general.md) | +| `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) | diff --git a/docs/art/views/spinner_menu_view.md b/docs/art/views/spinner_menu_view.md index 42c88e14..0f7139f8 100644 --- a/docs/art/views/spinner_menu_view.md +++ b/docs/art/views/spinner_menu_view.md @@ -11,14 +11,14 @@ Items can be selected on a menu via the cursor keys or by selecting them via a ` :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 [Art](../general.md) for general information on how to use views and common configuration properties available for them. +: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 [Art](../general.md) | -| `focusTextStyle` | Sets focus text style. See **Text Styles** in [Art](../general.md)| +| `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** | @@ -26,10 +26,10 @@ Items can be selected on a menu via the cursor keys or by selecting them via a ` | `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 [Art](../general.md) | +| `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 [Art](../general.md) | +| `focusItemFormat` | Sets the format for a focused list entry. See **Entry Formatting** in [MCI](../mci.md) | ### Hot Keys diff --git a/docs/art/views/text_view.md b/docs/art/views/text_view.md index 6f7f36b8..3bec8ed8 100644 --- a/docs/art/views/text_view.md +++ b/docs/art/views/text_view.md @@ -9,14 +9,14 @@ A text label view supports displaying simple text on a screen. :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 [Art](../general.md) for general information on how to use views and common configuration properties available for them. +: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 [Art](../general.md) | +| `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 | diff --git a/docs/art/views/vertical_menu_view.md b/docs/art/views/vertical_menu_view.md index 61654746..e46f92ae 100644 --- a/docs/art/views/vertical_menu_view.md +++ b/docs/art/views/vertical_menu_view.md @@ -11,14 +11,14 @@ Items can be selected on a menu via the cursor keys, Page Up, Page Down, Home, a :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 [Art](../general.md) for general information on how to use views and common configuration properties available for them. +: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 [Art](../general.md) | -| `focusTextStyle` | Sets focus text style. See **Text Styles** in [Art](../general.md)| +| `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 | @@ -27,10 +27,10 @@ Items can be selected on a menu via the cursor keys, Page Up, Page Down, Home, a | `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 [Art](../general.md) | +| `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 [Art](../general.md) | +| `focusItemFormat` | Sets the format for a focused list entry. See **Entry Formatting** in [MCI](../mci.md) | ### Hot Keys diff --git a/docs/assets/images/mask_edit_text_view_example1.gif b/docs/assets/images/mask_edit_text_view_example1.gif index 443d2868de13424a671d70e892d89da35a8fe789..b12ba5ab2b91c785886fb6e39b249c4a425f10ab 100644 GIT binary patch literal 7222 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 OzAD_?m3(`1IP@>56y!+& literal 7117 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 x$kW3TQTIt#p=lKTS+|?fRwRz~{Az Date: Fri, 28 Jan 2022 17:53:20 -0600 Subject: [PATCH 088/146] Added MT and TM --- docs/art/views/multi_line_edit_text_view.md | 53 +++++++++++ docs/art/views/toggle_menu_view.md | 83 ++++++++++++++++++ .../multi_line_edit_text_view_example1.gif | Bin 0 -> 13256 bytes 3 files changed, 136 insertions(+) create mode 100644 docs/art/views/multi_line_edit_text_view.md create mode 100644 docs/art/views/toggle_menu_view.md create mode 100644 docs/assets/images/multi_line_edit_text_view_example1.gif diff --git a/docs/art/views/multi_line_edit_text_view.md b/docs/art/views/multi_line_edit_text_view.md new file mode 100644 index 00000000..870360ba --- /dev/null +++ b/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/art/views/toggle_menu_view.md b/docs/art/views/toggle_menu_view.md new file mode 100644 index 00000000..65c1eabd --- /dev/null +++ b/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/assets/images/multi_line_edit_text_view_example1.gif b/docs/assets/images/multi_line_edit_text_view_example1.gif new file mode 100644 index 0000000000000000000000000000000000000000..acba094d96e0818938277fb1824b367dff3ef899 GIT binary patch literal 13256 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 Date: Fri, 28 Jan 2022 17:56:16 -0600 Subject: [PATCH 089/146] Added missing image --- docs/assets/images/toggle_menu_view_example1.gif | Bin 0 -> 3639 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/assets/images/toggle_menu_view_example1.gif diff --git a/docs/assets/images/toggle_menu_view_example1.gif b/docs/assets/images/toggle_menu_view_example1.gif new file mode 100644 index 0000000000000000000000000000000000000000..3944f658ce9ea9a512c6a9c3946205719fa47ffd GIT binary patch literal 3639 zcmeI!`8(9_8VB%u#%#74VltL*)-WM!M(d0%q#9#s2n`AeDT-3j5MzsyWIJ{VX$XlD z!^uv=5Ti}#+n!2X<&^K|T<3@L51i{dKYV+x*Kf}+&+}Z*`@Zj;_O?_DA3RDImQY53 ztQ9~3pin3@fEYqd3XuS+GK8SPNh;e18mu{W!tuG z+W~g$*g*rJ)9ID~RshxrYinBoTU*_Y6?2m}ZW z3=9GY1_%iW2?YoZ4Lu4F1`r+|9sv*$5pe?GB;w@B$jHbjfYYZ>M@L7W1vrN|cP<7X zCMG5p;5)6+9DGBN=&Gc&UQt^#Bu zva@q@bMpZ5^YaS;3JMAe0g8%>iUCSWN=gCB0LlR>5ET{I0V)xdl{WyY0BUP%Z{51Z zKYmv?%cW4+}zyK($d=6djJ0YwzjtR_I5s>-__OC)6>)2+uPUIH#j&rG&D3k zJS-3hMn*iJ3Bi!H}~q* zt2b}nynFZV{rmSHK79D}>C^oD{O8Y~zkK~NThSRd#s-FBC?pPQ-!WJTBofFE1o(GMkeB}*lHVcu`-X&obO?g79j~Y( zjb}w2`fJ)fBCE@tgvFNO70JbN@i68-Jga8RLFs9SdYmNB(J4dyNQi)rS2&xo+u}() z%85LX>g-`Ucx7A+)--djnGck)a%ER^u^i14V_7)5QSBU?eq6KuD#hj+TcF3iWPHh# z`;)^#HC@5QlDSEl-LwXsNnfTmLsnxgcI<+Uy!0PJx-6QHwpS8&6k<&0Sbkj@+(|W} zjDvBK8r|H^LW`;BiR&jF8hP!7^->q2#O>zS3)XSXtYj|DHH$+&S3gO?JVJEpPp8tF z6Y=z3Gr6Y`tHpf|Tc>W8xN-P6b2Fphl6SI81uyttx#f}g9EO#&=4<~-E3w)069)2G zKm7AE-9%N@wjGq}LLOsN#@f8g*Q+ClWQGXW(PR-^c0=)qVtsE=pYBa`fm2??qYom6 z0avA-)|3{S5>!`xQ6iMlcX6tQ<~y4;=~2@|iKDzVcFC0|i7)Ha&1Gox)1FU6AKmLA zB=pk~SjscrKrAt8T>X>7(p;;1riykU)>+giNHwfuP<~+UOE+(;277BXY*siTaBZ2eU&KCj+nF zToeW0`LH+@)~&zvB;s+%($lCy67jzc0f*8-`U27;2pAB%g#L%eeoPf|u5+sS4kcqSQ4sxRG0v8wB${mY}Uda=H7cq(9rBxIH%s zub!I2Lla_6zRFh@Qn!@pxo7C~Wm;wOJzHE}Ia-pWp5>E^dIsG5R2}#^LQQ7Lfiz>4 z<&if7+RVEi7hEYN;A05jnms^#fRpWWrMuMJGD^Ihgj*eY3D)rBO!i9CUSu^YNa!g0( z9M}(Q$Zi3RMK&{fXE%R)3$2dY%SK^%dttyq3s5$6PA@yMkmaKFH8owTT2LzNrrJS7PBlH^b5PoF=jdwao4x$7N&j^x6igu z%N`MW$I+K*YTi{Oo$F7f) z2Q6NjPXC#+kn|Pnuy3Kc_+&-eZCk4g@pxTFFC5S>pK6QY~JQYRz)TZIx-tk}soh|(UliuxF+sS3j^P{q+yE}=?bg@pk Qbot+DL Date: Fri, 28 Jan 2022 18:05:48 -0600 Subject: [PATCH 090/146] Navigation for new items --- docs/_includes/nav.md | 6 ++++++ docs/art/mci.md | 13 +++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md index a0788ddc..af7c2e2c 100644 --- a/docs/_includes/nav.md +++ b/docs/_includes/nav.md @@ -50,9 +50,15 @@ - [Themes]({{ site.baseurl }}{% link art/themes.md %}) - [MCI Codes]({{ site.baseurl }}{% link art/mci.md %}) - Views + - [Button]({{ site.baseurl }}{% link art/views/button_view.md %}) + - [Edit Text]({{ site.baseurl }}{% link art/views/edit_text_view.md %}) - [Full Menu]({{ site.baseurl }}{% link art/views/full_menu_view.md %}) - [Horizontal Menu]({{ site.baseurl }}{% link art/views/horizontal_menu_view.md %}) + - [Mask Edit Text]({{ site.baseurl }}{% link art/views/mask_edit_text_view.md %}) + - [Predefined Label]({{ site.baseurl }}{% link art/views/predefined_label_view.md %}) - [Spinner Menu]({{ site.baseurl }}{% link art/views/spinner_menu_view.md %}) + - [Text]({{ site.baseurl }}{% link art/views/text_view.md %}) + - [Toggle Menu]({{ site.baseurl }}{% link art/views/toggle_menu_view.md %}) - [Vertical Menu]({{ site.baseurl }}{% link art/views/vertical_menu_view.md %}) - Servers diff --git a/docs/art/mci.md b/docs/art/mci.md index bdd69056..d6dc4e9d 100644 --- a/docs/art/mci.md +++ b/docs/art/mci.md @@ -116,16 +116,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 | +| `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 | +| `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. From 7292ffe752ddb602468650b6162c0d15e57a2e1b Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Fri, 28 Jan 2022 18:08:22 -0600 Subject: [PATCH 091/146] Fixed header --- docs/art/views/button_view.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/art/views/button_view.md b/docs/art/views/button_view.md index 97dc97e8..65a753ca 100644 --- a/docs/art/views/button_view.md +++ b/docs/art/views/button_view.md @@ -1,8 +1,8 @@ --- layout: page -title: Full Menu View +title: Button View --- -## Full Menu View +## Button View A button view supports displaying a button on a screen. ## General Information From 4228a064f2d2a60578f7b8e671b4582b23656c17 Mon Sep 17 00:00:00 2001 From: MeaTLoTioN Date: Sat, 29 Jan 2022 11:40:13 +0000 Subject: [PATCH 092/146] fix pm's in enigma w00t --- core/mrc.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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': { From 50cdf693da4d6dc18619263b37c3b4dc75af8bb8 Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Sat, 29 Jan 2022 19:17:16 +0200 Subject: [PATCH 093/146] no clobber after additional testing. in case existing files but no config hjson there could be data overwritten due to copy being a big bully will not overwrite any files with -n and should be safe again . --- docker/bin/docker-entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/bin/docker-entrypoint.sh b/docker/bin/docker-entrypoint.sh index 1a6c6e3d..3ee86be0 100644 --- a/docker/bin/docker-entrypoint.sh +++ b/docker/bin/docker-entrypoint.sh @@ -11,7 +11,7 @@ if [[ ! -f $bbspath/config/$configname ]]; then for dir in "${prepopvols[@]}" do if [ -n "$(find "$bbspath/$dir" -maxdepth 0 -type d -empty 2>/dev/null)" ]; then - cp -rp $bbsstgp/$dir/* $bbspath/$dir/ + cp -rpn $bbsstgp/$dir/* $bbspath/$dir/ else echo "WARN skipped $bbspath/$dir - vol Not empty/not a new setup - possible bad state" fi From 7b2e478ca6182a5bafce1f31aca9272d8bbadbf1 Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Sat, 29 Jan 2022 19:42:37 +0200 Subject: [PATCH 094/146] Update docker-entrypoint.sh does not work recursively --- docker/bin/docker-entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/bin/docker-entrypoint.sh b/docker/bin/docker-entrypoint.sh index 3ee86be0..1a6c6e3d 100644 --- a/docker/bin/docker-entrypoint.sh +++ b/docker/bin/docker-entrypoint.sh @@ -11,7 +11,7 @@ if [[ ! -f $bbspath/config/$configname ]]; then for dir in "${prepopvols[@]}" do if [ -n "$(find "$bbspath/$dir" -maxdepth 0 -type d -empty 2>/dev/null)" ]; then - cp -rpn $bbsstgp/$dir/* $bbspath/$dir/ + cp -rp $bbsstgp/$dir/* $bbspath/$dir/ else echo "WARN skipped $bbspath/$dir - vol Not empty/not a new setup - possible bad state" fi From 65b9c570ee53e2da5884894fa0c818992696b783 Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Sat, 29 Jan 2022 20:17:30 +0200 Subject: [PATCH 095/146] updating according to PR --- docker/bin/docker-entrypoint.sh | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/docker/bin/docker-entrypoint.sh b/docker/bin/docker-entrypoint.sh index 1a6c6e3d..29e4d917 100644 --- a/docker/bin/docker-entrypoint.sh +++ b/docker/bin/docker-entrypoint.sh @@ -1,25 +1,28 @@ #!/usr/bin/env bash +set -e # Set some vars -prepopvols=("config" "mods" "art") # these are folders which contain runtime needed files, and need to be represented in the host -bbspath=/enigma-bbs # install location -bbsstgp=/enigma-bbs-pre # staging location for prepopvals -configname=config.hjson # this is the default name, this script is intended for easy get-go - make changes as needed +PRE_POP_VOLS=("config" "mods" "art") # these are folders which contain runtime needed files, and need to be represented in the host +BBS_ROOT=/enigma-bbs # install location +BBS_STG_P=/enigma-bbs-pre # staging location for pre populated volumes (PRE_POP_VOLS) +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 $bbspath/config/$configname ]]; then - for dir in "${prepopvols[@]}" +if [[ ! -f $BBS_ROOT/config/$CONFIG_NAME ]]; then + for DIR in "${PRE_POP_VOLS[@]}" do - if [ -n "$(find "$bbspath/$dir" -maxdepth 0 -type d -empty 2>/dev/null)" ]; then - cp -rp $bbsstgp/$dir/* $bbspath/$dir/ + if [ -n "$(find "$BBS_ROOT/$DIR" -maxdepth 0 -type d -empty 2>/dev/null)" ]; then + cp -rp $BBS_STG_P/$DIR/* $BBS_ROOT/$DIR/ else - echo "WARN skipped $bbspath/$dir - vol Not empty/not a new setup - possible bad state" + printf "WARN: skipped $BBS_ROOT/$DIR: 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 $bbspath/config/$configname ]]; then #make sure once more, otherwise pm2-runtime will loop if missing the config - echo "for some reason you have skipped configuration - enigma will not work. please run config" +if [[ ! -f $BBS_ROOT/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 pm2-runtime main.js From 7d56ee3cc6d4fb67cc68481ae77bd5aac407d145 Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Sat, 29 Jan 2022 21:16:02 +0200 Subject: [PATCH 096/146] s/you/ You --- docker/bin/docker-entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/bin/docker-entrypoint.sh b/docker/bin/docker-entrypoint.sh index 29e4d917..575b104d 100644 --- a/docker/bin/docker-entrypoint.sh +++ b/docker/bin/docker-entrypoint.sh @@ -15,7 +15,7 @@ if [[ ! -f $BBS_ROOT/config/$CONFIG_NAME ]]; then cp -rp $BBS_STG_P/$DIR/* $BBS_ROOT/$DIR/ else printf "WARN: skipped $BBS_ROOT/$DIR: 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" + 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 From e814ccce31ff8e3f29f2295125e2e981890c1fb2 Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Sat, 29 Jan 2022 21:19:58 +0200 Subject: [PATCH 097/146] ENiGMA<3 --- docs/installation/docker.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/installation/docker.md b/docs/installation/docker.md index 4b618ddb..0acd5d85 100644 --- a/docs/installation/docker.md +++ b/docs/installation/docker.md @@ -11,7 +11,7 @@ prepare a folder where you are going to save your bbs files. 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" \ +--name " ENiGMABBS" \ -v "$(pwd)/config:/enigma-bbs/config" \ -v "$(pwd)/db:/enigma-bbs/db" \ -v "$(pwd)/logs:/enigma-bbs/logs" \ @@ -25,7 +25,7 @@ enigmabbs/enigmabbs:latest 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" \ +--name "ENiGMABBS" \ -v "$(pwd)/config:/enigma-bbs/config" \ -v "$(pwd)/db:/enigma-bbs/db" \ -v "$(pwd)/logs:/enigma-bbs/logs" \ @@ -36,9 +36,9 @@ docker run -d -p 8888:8888 \ enigmabbs/enigmabbs: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. +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``` +```docker restart ENiGMABBS``` :bulb: Configuration will be stored in `$(pwd)/enigma-bbs/config`. From d46adfc74362e50c8f3ff50fe009a6ece11d0933 Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Sat, 29 Jan 2022 21:21:38 +0200 Subject: [PATCH 098/146] comments --- docker/bin/docker-entrypoint.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docker/bin/docker-entrypoint.sh b/docker/bin/docker-entrypoint.sh index 575b104d..d2b33dc0 100644 --- a/docker/bin/docker-entrypoint.sh +++ b/docker/bin/docker-entrypoint.sh @@ -2,10 +2,10 @@ set -e # Set some vars -PRE_POP_VOLS=("config" "mods" "art") # these are folders which contain runtime needed files, and need to be represented in the host -BBS_ROOT=/enigma-bbs # install location -BBS_STG_P=/enigma-bbs-pre # staging location for pre populated volumes (PRE_POP_VOLS) -CONFIG_NAME=config.hjson # this is the default name, this script is intended for easy get-go - make changes as needed +PRE_POP_VOLS=("config" "mods" "art") # These are folders which contain runtime needed files, and need to be represented in the host +BBS_ROOT=/enigma-bbs # Install location +BBS_STG_P=/enigma-bbs-pre # Staging location for pre populated volumes (PRE_POP_VOLS) +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/config/$CONFIG_NAME ]]; then @@ -20,7 +20,7 @@ if [[ ! -f $BBS_ROOT/config/$CONFIG_NAME ]]; then done ./oputil.js config new fi -if [[ ! -f $BBS_ROOT/config/$CONFIG_NAME ]]; then #make sure once more, otherwise pm2-runtime will loop if missing the config +if [[ ! -f $BBS_ROOT/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 From 39efa79743262e08bfa323e19b86e64a1a4de3ec Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Sat, 29 Jan 2022 21:24:20 +0200 Subject: [PATCH 099/146] verbose vars --- docker/bin/docker-entrypoint.sh | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docker/bin/docker-entrypoint.sh b/docker/bin/docker-entrypoint.sh index d2b33dc0..34ff8cfa 100644 --- a/docker/bin/docker-entrypoint.sh +++ b/docker/bin/docker-entrypoint.sh @@ -2,25 +2,25 @@ set -e # Set some vars -PRE_POP_VOLS=("config" "mods" "art") # These are folders which contain runtime needed files, and need to be represented in the host -BBS_ROOT=/enigma-bbs # Install location -BBS_STG_P=/enigma-bbs-pre # Staging location for pre populated volumes (PRE_POP_VOLS) +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/config/$CONFIG_NAME ]]; then - for DIR in "${PRE_POP_VOLS[@]}" +if [[ ! -f $BBS_ROOT_DIR/config/$CONFIG_NAME ]]; then + for DIR in "${PRE_POPULATED_VOLUMES[@]}" do - if [ -n "$(find "$BBS_ROOT/$DIR" -maxdepth 0 -type d -empty 2>/dev/null)" ]; then - cp -rp $BBS_STG_P/$DIR/* $BBS_ROOT/$DIR/ + if [ -n "$(find "$BBS_ROOT_DIR/$DIR" -maxdepth 0 -type d -empty 2>/dev/null)" ]; then + cp -rp $BBS_STAGING_PATH/$DIR/* $BBS_ROOT_DIR/$DIR/ else - printf "WARN: skipped $BBS_ROOT/$DIR: Volume not empty or not a new setup; Files required to run ENiGMA 1/2 may be missing.\n Possible bad state\n" + printf "WARN: skipped $BBS_ROOT_DIR/$DIR: 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/config/$CONFIG_NAME ]]; then # Make sure once more, otherwise pm2-runtime will loop if missing the config +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 From 6e2cf64843a69d79dfebd3d043a1e89f728767f5 Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Sat, 29 Jan 2022 21:28:14 +0200 Subject: [PATCH 100/146] varexpansion --- docker/bin/docker-entrypoint.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docker/bin/docker-entrypoint.sh b/docker/bin/docker-entrypoint.sh index 34ff8cfa..c17dc8b2 100644 --- a/docker/bin/docker-entrypoint.sh +++ b/docker/bin/docker-entrypoint.sh @@ -9,12 +9,12 @@ CONFIG_NAME=config.hjson # This is the default name, this script is intended for # Setup happens when there is no existing config file if [[ ! -f $BBS_ROOT_DIR/config/$CONFIG_NAME ]]; then - for DIR in "${PRE_POPULATED_VOLUMES[@]}" + for VOLUME in "${PRE_POPULATED_VOLUMES[@]}" do - if [ -n "$(find "$BBS_ROOT_DIR/$DIR" -maxdepth 0 -type d -empty 2>/dev/null)" ]; then - cp -rp $BBS_STAGING_PATH/$DIR/* $BBS_ROOT_DIR/$DIR/ + 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/$DIR: Volume not empty or not a new setup; Files required to run ENiGMA 1/2 may be missing.\n Possible bad state\n" + 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 From 7bbb2104b9644097a06959d32eb8775fc0981cb7 Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Sat, 29 Jan 2022 21:59:20 +0200 Subject: [PATCH 101/146] I learn fast, but need slow explaining --- docker/bin/docker-entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/bin/docker-entrypoint.sh b/docker/bin/docker-entrypoint.sh index c17dc8b2..5f83581c 100644 --- a/docker/bin/docker-entrypoint.sh +++ b/docker/bin/docker-entrypoint.sh @@ -25,5 +25,5 @@ if [[ ! -f $BBS_ROOT_DIR/config/$CONFIG_NAME ]]; then # Make sure once more, oth exit 1 else - pm2-runtime main.js + exec pm2-runtime main.js fi From a5128a504b844fbb29f782f3a561df225e788424 Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Sat, 29 Jan 2022 23:04:29 +0200 Subject: [PATCH 102/146] exec again --- docker/bin/docker-entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/bin/docker-entrypoint.sh b/docker/bin/docker-entrypoint.sh index 5f83581c..e96eeed9 100644 --- a/docker/bin/docker-entrypoint.sh +++ b/docker/bin/docker-entrypoint.sh @@ -25,5 +25,5 @@ if [[ ! -f $BBS_ROOT_DIR/config/$CONFIG_NAME ]]; then # Make sure once more, oth exit 1 else - exec pm2-runtime main.js + exec pm2-runtime main.js fi From d4098ed114ffb7da49de4e2fd86da3e4adc13a79 Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Sat, 29 Jan 2022 21:27:03 -0600 Subject: [PATCH 103/146] Additional fixes from code review comments --- core/button_view.js | 3 +-- core/fse.js | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/core/button_view.js b/core/button_view.js index a4f70744..2aebf347 100644 --- a/core/button_view.js +++ b/core/button_view.js @@ -21,8 +21,7 @@ function ButtonView(options) { util.inherits(ButtonView, TextView); ButtonView.prototype.onKeyPress = function(ch, key) { - let actionForKeyName = key ? key.name : ch; - if(this.isKeyMapped('accept', actionForKeyName) || ' ' === ch) { + if(this.isKeyMapped('accept', (key ? key.name : ch)) || ' ' === ch) { this.submitData = 'accept'; this.emit('action', 'accept'); delete this.submitData; diff --git a/core/fse.js b/core/fse.js index e66d40fe..361bbeff 100644 --- a/core/fse.js +++ b/core/fse.js @@ -792,7 +792,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul }, function prepareViewStates(callback) { let from = self.viewControllers.header.getView(MciViewIds.header.from); - if (from !== undefined) { + if (from) { from.acceptsFocus = false; } @@ -918,7 +918,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul initHeaderViewMode() { // Only set header text for from view if it is on the form - if (self.viewControllers.header.getView(MciViewIds.header.from) !== undefined) { + 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); From 0bb985ec0f63c0c1bb3e9823af1017611f20c0e9 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Mon, 31 Jan 2022 22:22:22 -0700 Subject: [PATCH 104/146] Update a number of dependencies * minor versions * patches / fixes --- package.json | 20 ++++----- yarn.lock | 122 ++++++++++++++++++++++++++++----------------------- 2 files changed, 77 insertions(+), 65 deletions(-) diff --git a/package.json b/package.json index c828edc4..de0a9f88 100644 --- a/package.json +++ b/package.json @@ -22,29 +22,29 @@ "retro" ], "dependencies": { - "@breejs/later": "^4.0.2", - "async": "3.2.0", + "@breejs/later": "4.1.0", + "async": "3.2.3", "binary-parser": "1.7.0", "buffers": "github:NuSkooler/node-buffers", "bunyan": "1.8.15", - "deepdash": "5.3.5", + "deepdash": "5.3.9", "exiftool": "^0.0.3", "fs-extra": "9.1.0", - "glob": "7.1.6", - "graceful-fs": "4.2.5", - "hashids": "2.2.8", + "glob": "7.2.0", + "graceful-fs": "4.2.9", + "hashids": "2.2.10", "hjson": "3.2.2", - "iconv-lite": "^0.6.2", + "iconv-lite": "0.6.3", "ini-config-parser": "^1.0.4", "inquirer": "7.3.3", "lodash": "4.17.21", "lru-cache": "^5.1.1", - "mime-types": "2.1.28", + "mime-types": "2.1.34", "minimist": "1.2.5", "moment": "2.29.1", "nntp-server": "^1.0.3", - "node-pty": "0.10.0", - "nodemailer": "6.4.17", + "node-pty": "0.10.1", + "nodemailer": "6.7.2", "otplib": "11.0.1", "qrcode-generator": "^1.4.4", "rlogin": "^1.0.0", diff --git a/yarn.lock b/yarn.lock index d979271b..69e7568f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23,10 +23,10 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@breejs/later@^4.0.2": - version "4.0.2" - resolved "https://registry.yarnpkg.com/@breejs/later/-/later-4.0.2.tgz#38c85cc98b717c7a196f87238090adaea01f8c9e" - integrity sha512-EN0SlbyYouBdtZis1htdsgGlwFePzkXPwdIeqaBaavxkJT1G2/bitc2LSixjv45z2njXslxlJI1mW2O/Gmrb+A== +"@breejs/later@4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@breejs/later/-/later-4.1.0.tgz#9246907f46cc9e9c9af37d791ab468d98921bcc1" + integrity sha512-QgGnZ9b7o4k0Ai1ZbTJWwZpZcFK9d+Gb+DyNt4UT9x6IEIs5HVu0iIlmgzGqN+t9MoJSpSPo9S/Mm51UtHr3JA== "@cnakazawa/watch@^1.0.3": version "1.0.3" @@ -238,10 +238,10 @@ astral-regex@^2.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== -async@3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" - integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw== +async@3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9" + integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g== at-least-node@^1.0.0: version "1.0.0" @@ -549,13 +549,13 @@ deep-is@^0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= -deepdash@5.3.5: - version "5.3.5" - resolved "https://registry.yarnpkg.com/deepdash/-/deepdash-5.3.5.tgz#611bec9c1f2829832d21971dcbefe712e408647d" - integrity sha512-1ZdPPCI1pCEqeAWGSw+Nbpb/2iIV4w3sGPc22H/PDtcApb8+psTzPIoOVD040iBaT2wqZab29Kjrz6G+cd5mqQ== +deepdash@5.3.9: + version "5.3.9" + resolved "https://registry.yarnpkg.com/deepdash/-/deepdash-5.3.9.tgz#2aa92570d7b1787ed281e2fafe0be24df00ddfe4" + integrity sha512-GRzJ0q9PDj2T+J2fX+b+TlUa2NlZ11l6vJ8LHNKVGeZ8CfxCuJaCychTq07iDRTvlfO8435jlvVS1QXBrW9kMg== dependencies: - lodash "^4.17.20" - lodash-es "^4.17.20" + lodash "^4.17.21" + lodash-es "^4.17.21" define-property@^0.2.5: version "0.2.5" @@ -992,10 +992,10 @@ glob-parent@^5.0.0: dependencies: is-glob "^4.0.1" -glob@7.1.6, glob@^7.1.3: - version "7.1.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== +glob@7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" @@ -1027,6 +1027,18 @@ glob@^7.0.3, glob@^7.0.5, glob@^7.1.2: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^7.1.3: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + globals@^12.1.0: version "12.4.0" resolved "https://registry.yarnpkg.com/globals/-/globals-12.4.0.tgz#a18813576a41b00a24a97e7f815918c2e19925f8" @@ -1045,10 +1057,10 @@ globby@^6.1.0: pify "^2.0.0" pinkie-promise "^2.0.0" -graceful-fs@4.2.5: - version "4.2.5" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.5.tgz#bc18864a6c9fc7b303f2e2abdb9155ad178fbe29" - integrity sha512-kBBSQbz2K0Nyn+31j/w36fUfxkBW9/gfwRWdUY1ULReH3iokVJgddZAFcD1D0xlgTmFxJCbUkUclAlc6/IDJkw== +graceful-fs@4.2.9: + version "4.2.9" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" + integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== graceful-fs@^4.1.6: version "4.1.11" @@ -1106,16 +1118,23 @@ has-values@^1.0.0: is-number "^3.0.0" kind-of "^4.0.0" -hashids@2.2.8: - version "2.2.8" - resolved "https://registry.yarnpkg.com/hashids/-/hashids-2.2.8.tgz#d1e4e1be48e711ad6e5f27b32c37c399d5eeb5b4" - integrity sha512-OI6rsk/OqyX60nBsqxnNl3R7CDv9eMOgxzipshZwXE6cUcBcEyHf8rNDlWVdQJZK3TEynILYybCsNUEQfJsdLA== +hashids@2.2.10: + version "2.2.10" + resolved "https://registry.yarnpkg.com/hashids/-/hashids-2.2.10.tgz#82f45538cf03ce73e31b78d1abe78d287cf760c4" + integrity sha512-nXnYums7F8B5Y+GSThutLPlKMaamW1yjWNZVt0WModiJfdjaDZHnhYTWblS+h1OoBx3yjwiBwxldPP3nIbFSSA== hjson@3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/hjson/-/hjson-3.2.2.tgz#a5a81138f4c0bb427e4b2ac917fafd4b454436cf" integrity sha512-MkUeB0cTIlppeSsndgESkfFD21T2nXPRaBStLtf3cAYA2bVEFdXlodZB0TukwZiobPD1Ksax5DK4RTZeaXCI3Q== +iconv-lite@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + iconv-lite@^0.4.24, iconv-lite@^0.4.4: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -1123,13 +1142,6 @@ iconv-lite@^0.4.24, iconv-lite@^0.4.4: dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.2.tgz#ce13d1875b0c3a674bd6a04b7f76b01b1b6ded01" - integrity sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ== - dependencies: - safer-buffer ">= 2.1.2 < 3.0.0" - ignore-walk@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8" @@ -1435,12 +1447,12 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -lodash-es@^4.17.20: - version "4.17.20" - resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.20.tgz#29f6332eefc60e849f869c264bc71126ad61e8f7" - integrity sha512-JD1COMZsq8maT6mnuz1UMV0jvYD0E0aUsSOdrr1/nAG3dhqQXwRRgeW0cSqH1U43INKcqxaiVIQNOUDld7gRDA== +lodash-es@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" + integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== -lodash@4.17.21, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20: +lodash@4.17.21, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -1490,17 +1502,17 @@ micromatch@^3.1.4: snapdragon "^0.8.1" to-regex "^3.0.2" -mime-db@1.45.0: - version "1.45.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea" - integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w== +mime-db@1.51.0: + version "1.51.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c" + integrity sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g== -mime-types@2.1.28: - version "2.1.28" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.28.tgz#1160c4757eab2c5363888e005273ecf79d2a0ecd" - integrity sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ== +mime-types@2.1.34: + version "2.1.34" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.34.tgz#5a712f9ec1503511a945803640fafe09d3793c24" + integrity sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A== dependencies: - mime-db "1.45.0" + mime-db "1.51.0" mimic-fn@^2.1.0: version "2.1.0" @@ -1681,17 +1693,17 @@ node-pre-gyp@^0.11.0: semver "^5.3.0" tar "^4" -node-pty@0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.10.0.tgz#c98d23967b076b35c9fb216c542a04d0b5db4821" - integrity sha512-Q65ookKbjhqWUYKmtZ6iPn0nnqNdzpm3YJOBmzwWJde/TrenBxK9FgqGGtSW0Wjz4YsR1grQF4a7RS5nBwuW9A== +node-pty@0.10.1: + version "0.10.1" + resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.10.1.tgz#cd05d03a2710315ec40221232ec04186f6ac2c6d" + integrity sha512-JTdtUS0Im/yRsWJSx7yiW9rtpfmxqxolrtnyKwPLI+6XqTAPW/O2MjS8FYL4I5TsMbH2lVgDb2VMjp+9LoQGNg== dependencies: nan "^2.14.0" -nodemailer@6.4.17: - version "6.4.17" - resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.4.17.tgz#8de98618028953b80680775770f937243a7d7877" - integrity sha512-89ps+SBGpo0D4Bi5ZrxcrCiRFaMmkCt+gItMXQGzEtZVR3uAD3QAQIDoxTWnx3ky0Dwwy/dhFrQ+6NNGXpw/qQ== +nodemailer@6.7.2: + version "6.7.2" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.7.2.tgz#44b2ad5f7ed71b7067f7a21c4fedabaec62b85e0" + integrity sha512-Dz7zVwlef4k5R71fdmxwR8Q39fiboGbu3xgswkzGwczUfjp873rVxt1O46+Fh0j1ORnAC6L9+heI8uUpO6DT7Q== nopt@^4.0.1: version "4.0.1" From 419d0d78f5098a2d9fc7c436bb97a0e56344018c Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 1 Feb 2022 00:25:59 -0700 Subject: [PATCH 105/146] Update binary-parser --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index de0a9f88..f6aa6833 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "dependencies": { "@breejs/later": "4.1.0", "async": "3.2.3", - "binary-parser": "1.7.0", + "binary-parser": "2.0.2", "buffers": "github:NuSkooler/node-buffers", "bunyan": "1.8.15", "deepdash": "5.3.9", diff --git a/yarn.lock b/yarn.lock index 69e7568f..be2cf2a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -283,10 +283,10 @@ binary-parser@1.6.2: resolved "https://registry.yarnpkg.com/binary-parser/-/binary-parser-1.6.2.tgz#8410a82ffd9403271ec182bd91e63a09cee88cbe" integrity sha512-cYAhKB51A9T/uylDvMK7uAYaPLWLwlferNOpnQ0E0fuO73yPi7kWaWiOm22BvuKxCbggmkiFN0VkuLg6gc+KQQ== -binary-parser@1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/binary-parser/-/binary-parser-1.7.0.tgz#117dbfb071dde44a4910ea072a8d32e1549e8e12" - integrity sha512-lIWQLmiEJtx2ZAUPbZWLA/0bgxYdWkySU9pZq5dNRcyzliZCBIwbgLeyydu57GFskGcZDbirMjPSgVcUgZpbdg== +binary-parser@2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/binary-parser/-/binary-parser-2.0.2.tgz#e494f9a0ebb2a4ec7d2b09df47cdeffa23df1563" + integrity sha512-F2JeaLmExQ0vOEbS5ERzkxePKWTPqJPV6Z04/owaXMQLlW1Kt9v2gUz3SocR40JQGtrUeZu3j6prZDeuEG8Dng== brace-expansion@^1.1.7: version "1.1.11" From 37a65f48402db0be13ca55cac636164bbec11ec5 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Tue, 1 Feb 2022 00:27:46 -0700 Subject: [PATCH 106/146] Update fs-extra --- package.json | 2 +- yarn.lock | 14 ++++---------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index f6aa6833..0d73de66 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "bunyan": "1.8.15", "deepdash": "5.3.9", "exiftool": "^0.0.3", - "fs-extra": "9.1.0", + "fs-extra": "10.0.0", "glob": "7.2.0", "graceful-fs": "4.2.9", "hashids": "2.2.10", diff --git a/yarn.lock b/yarn.lock index be2cf2a2..adfe4878 100644 --- a/yarn.lock +++ b/yarn.lock @@ -243,11 +243,6 @@ async@3.2.3: resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9" integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g== -at-least-node@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" - integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== - atob@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" @@ -932,12 +927,11 @@ from2@^2.3.0: inherits "^2.0.1" readable-stream "^2.0.0" -fs-extra@9.1.0: - version "9.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" - integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== +fs-extra@10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1" + integrity sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ== dependencies: - at-least-node "^1.0.0" graceful-fs "^4.2.0" jsonfile "^6.0.1" universalify "^2.0.0" From 9f246b17040b819c7e3591a03951eaf00907ea8e Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Tue, 1 Feb 2022 12:25:36 -0600 Subject: [PATCH 107/146] Updated versions for Jekyll --- docs/Gemfile | 15 +-- docs/Gemfile.lock | 309 +++++++++++++++++++++++++++++++++++++--------- 2 files changed, 257 insertions(+), 67 deletions(-) diff --git a/docs/Gemfile b/docs/Gemfile index 1a0104dd..53021488 100644 --- a/docs/Gemfile +++ b/docs/Gemfile @@ -8,10 +8,11 @@ source "https://rubygems.org" # # This will help ensure the proper Jekyll version is running. # Happy Jekylling! -gem "jekyll", "~> 3.7.0" +#gem "jekyll", "~> 3.7.0" +gem "github-pages", "~> 223", group: :jekyll_plugins # 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 +20,11 @@ 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' 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..b2629765 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -1,101 +1,290 @@ GEM remote: https://rubygems.org/ specs: - activesupport (4.2.9) - i18n (~> 0.7) + activesupport (6.0.4.4) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 0.7, < 2) minitest (~> 5.1) - thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) - addressable (2.5.2) - public_suffix (>= 2.0.2, < 4.0) + zeitwerk (~> 2.2, >= 2.2.2) + addressable (2.8.0) + public_suffix (>= 2.0.2, < 5.0) + coffee-script (2.4.1) + coffee-script-source + execjs + coffee-script-source (1.11.1) colorator (1.1.0) - concurrent-ruby (1.0.5) - em-websocket (0.5.1) + commonmarker (0.17.13) + ruby-enum (~> 0.5) + concurrent-ruby (1.1.9) + dnsruby (1.61.9) + simpleidn (~> 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) + ethon (0.15.0) + ffi (>= 1.15.0) + eventmachine (1.2.7) + execjs (2.8.1) + faraday (1.9.3) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.0) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.0.3) + multipart-post (>= 1.2, < 3) + faraday-net_http (1.0.1) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.3) + 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) + github-pages (223) + github-pages-health-check (= 1.17.9) + jekyll (= 3.9.0) + jekyll-avatar (= 0.7.0) + jekyll-coffeescript (= 1.1.1) + jekyll-commonmark-ghpages (= 0.1.6) + jekyll-default-layout (= 0.1.4) + jekyll-feed (= 0.15.1) + jekyll-gist (= 1.5.0) + jekyll-github-metadata (= 2.13.0) + jekyll-include-cache (= 0.2.1) + jekyll-mentions (= 1.6.0) + jekyll-optional-front-matter (= 0.3.2) + jekyll-paginate (= 1.1.0) + jekyll-readme-index (= 0.3.0) + jekyll-redirect-from (= 0.16.0) + jekyll-relative-links (= 0.6.1) + jekyll-remote-theme (= 0.4.3) + jekyll-sass-converter (= 1.5.2) + jekyll-seo-tag (= 2.7.1) + jekyll-sitemap (= 1.4.0) + jekyll-swiss (= 1.0.0) + jekyll-theme-architect (= 0.2.0) + jekyll-theme-cayman (= 0.2.0) + jekyll-theme-dinky (= 0.2.0) + jekyll-theme-hacker (= 0.2.0) + jekyll-theme-leap-day (= 0.2.0) + jekyll-theme-merlot (= 0.2.0) + jekyll-theme-midnight (= 0.2.0) + jekyll-theme-minimal (= 0.2.0) + jekyll-theme-modernist (= 0.2.0) + jekyll-theme-primer (= 0.6.0) + jekyll-theme-slate (= 0.2.0) + jekyll-theme-tactile (= 0.2.0) + jekyll-theme-time-machine (= 0.2.0) + jekyll-titles-from-headings (= 0.5.3) + jemoji (= 0.12.0) + kramdown (= 2.3.1) + kramdown-parser-gfm (= 1.1.0) + liquid (= 4.0.3) + mercenary (~> 0.3) + minima (= 2.5.1) + nokogiri (>= 1.12.5, < 2.0) + rouge (= 3.26.0) + terminal-table (~> 1.4) + github-pages-health-check (1.17.9) + addressable (~> 2.3) + dnsruby (~> 1.60) + octokit (~> 4.0) + public_suffix (>= 3.0, < 5.0) + typhoeus (~> 1.3) + html-pipeline (2.14.0) activesupport (>= 2) - nokogiri (>= 1.8.5) - http_parser.rb (0.6.0) - i18n (0.9.1) + nokogiri (>= 1.4) + http_parser.rb (0.8.0) + i18n (0.9.5) concurrent-ruby (~> 1.0) - jekyll (3.7.4) + jekyll (3.9.0) addressable (~> 2.4) colorator (~> 1.0) em-websocket (~> 0.5) i18n (~> 0.7) jekyll-sass-converter (~> 1.0) jekyll-watch (~> 2.0) - kramdown (~> 1.14) + kramdown (>= 1.17, < 3) liquid (~> 4.0) mercenary (~> 0.3.3) pathutil (~> 0.9) rouge (>= 1.7, < 4) safe_yaml (~> 1.0) - jekyll-feed (0.9.2) - jekyll (~> 3.3) - jekyll-sass-converter (1.5.1) + jekyll-avatar (0.7.0) + jekyll (>= 3.0, < 5.0) + jekyll-coffeescript (1.1.1) + coffee-script (~> 2.2) + coffee-script-source (~> 1.11.1) + jekyll-commonmark (1.3.1) + commonmarker (~> 0.14) + jekyll (>= 3.7, < 5.0) + jekyll-commonmark-ghpages (0.1.6) + commonmarker (~> 0.17.6) + jekyll-commonmark (~> 1.2) + rouge (>= 2.0, < 4.0) + jekyll-default-layout (0.1.4) + jekyll (~> 3.0) + jekyll-feed (0.15.1) + jekyll (>= 3.7, < 5.0) + jekyll-gist (1.5.0) + octokit (~> 4.2) + jekyll-github-metadata (2.13.0) + jekyll (>= 3.4, < 5.0) + octokit (~> 4.0, != 4.4.0) + jekyll-include-cache (0.2.1) + jekyll (>= 3.7, < 5.0) + jekyll-mentions (1.6.0) + html-pipeline (~> 2.3) + jekyll (>= 3.7, < 5.0) + jekyll-optional-front-matter (0.3.2) + jekyll (>= 3.0, < 5.0) + jekyll-paginate (1.1.0) + jekyll-readme-index (0.3.0) + jekyll (>= 3.0, < 5.0) + jekyll-redirect-from (0.16.0) + jekyll (>= 3.3, < 5.0) + jekyll-relative-links (0.6.1) + jekyll (>= 3.3, < 5.0) + jekyll-remote-theme (0.4.3) + addressable (~> 2.0) + jekyll (>= 3.5, < 5.0) + jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0) + rubyzip (>= 1.3.0, < 3.0) + jekyll-sass-converter (1.5.2) 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) + jekyll-seo-tag (2.7.1) + jekyll (>= 3.8, < 5.0) + jekyll-sitemap (1.4.0) + jekyll (>= 3.7, < 5.0) + jekyll-swiss (1.0.0) + jekyll-theme-architect (0.2.0) + jekyll (> 3.5, < 5.0) jekyll-seo-tag (~> 2.0) - jekyll-watch (2.0.0) + jekyll-theme-cayman (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-dinky (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-hacker (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-leap-day (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-merlot (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-midnight (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-minimal (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-modernist (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-primer (0.6.0) + jekyll (> 3.5, < 5.0) + jekyll-github-metadata (~> 2.9) + jekyll-seo-tag (~> 2.0) + jekyll-theme-slate (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-tactile (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-time-machine (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-titles-from-headings (0.5.3) + jekyll (>= 3.3, < 5.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) + jekyll (>= 3.0, < 5.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.3.6) - mini_portile2 (2.4.0) - minitest (5.11.1) - nokogiri (1.10.8) - mini_portile2 (~> 2.4.0) - pathutil (0.16.1) + minima (2.5.1) + jekyll (>= 3.5, < 5.0) + jekyll-feed (~> 0.9) + jekyll-seo-tag (~> 2.1) + minitest (5.15.0) + multipart-post (2.1.1) + nokogiri (1.13.1-x86_64-linux) + racc (~> 1.4) + octokit (4.22.0) + faraday (>= 0.9) + sawyer (~> 0.8.0, >= 0.5.3) + 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) + 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.26.0) + ruby-enum (0.9.0) + i18n + ruby2_keywords (0.0.5) + rubyzip (2.3.2) + safe_yaml (1.0.5) + sass (3.7.4) sass-listen (~> 4.0.0) sass-listen (4.0.0) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) + sawyer (0.8.2) + addressable (>= 2.3.5) + faraday (> 0.8, < 2.0) + simpleidn (0.2.1) + unf (~> 0.1.4) + terminal-table (1.8.0) + unicode-display_width (~> 1.1, >= 1.1.1) thread_safe (0.3.6) - tzinfo (1.2.4) + typhoeus (1.4.0) + ethon (>= 0.9.0) + tzinfo (1.2.9) thread_safe (~> 0.1) + unf (0.1.4) + unf_ext + unf_ext (0.0.8) + unicode-display_width (1.8.0) + zeitwerk (2.5.4) PLATFORMS - ruby + x86_64-linux DEPENDENCIES - hacker - jekyll (~> 3.7.0) - jekyll-feed (~> 0.6) - jekyll-seo-tag - jekyll-sitemap - jekyll-theme-hacker - jemoji + github-pages (~> 223) + 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 From 0725549a1a2362591906149e8d7a68b1be5101ff Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Tue, 1 Feb 2022 18:00:27 -0600 Subject: [PATCH 108/146] Made side panel more mobile friendly --- docs/_layouts/default.html | 14 +++++++++----- docs/assets/css/style.scss | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html index 60f87db8..494eb6e2 100644 --- a/docs/_layouts/default.html +++ b/docs/_layouts/default.html @@ -9,21 +9,25 @@ -
Fork me on GitHub + Fork me on GitHub +
-
-
+
+
+ MENU {{ content }}
+
-
+
{% if site.google_analytics %}