From 7690280f7ac0b31040eef3cceb2ecf5e3b5cdf18 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 20 Mar 2017 22:01:09 -0600 Subject: [PATCH] terminal-forms.js --- README.md | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ index.js | 35 ++++++++++++++---- 2 files changed, 134 insertions(+), 7 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..a5b420c --- /dev/null +++ b/README.md @@ -0,0 +1,106 @@ +Terminal Forms (js) +============== + +You give it a TTY, it gives you the best form-handling that it knows how! + +``` +var form = require('terminal-forms.js').create(process.stdin, process.stdout); +``` + +Input types presently supported: + +* `form.ask(prompt, handlers)` + * i.e. `form.ask("What is your quest? ", form.inputs.text).then(fn);` +* `form.setStatus(msg)` + * i.e. `form.setStatus("(hint: you seek the Grail!)")` +* `form.inputs` + * `.text` (no constraints) + * `.email` (checks format and looks up MX records) + * `.url` (checks format and looks up A/AAAA/CNAME records) + +Handlers +======== + +A handler may implement any or all of these interfaces: + +* `onReturnAsync(rs, ws, input, ch)` +* `onDebounceAsync(rs, ws, input, ch)` + +The follow options may also be specified: + +* `debounceTimeout: ms` + +``` +{ onReturnAsync: function (rs, ws, input, ch) { + // 1. pause the read stream if needed + + // 2. the write stream is given as a convenience for clearing the newline, etc + + // 3. check that input as a whole is valid + + // 4. check the most recent character, if desired + + // 5. normalize the input if desired (i.e. John.Doe@GMail.com -> john.doe@gmail.com) + + // You can error out + // return form.PromiseA.reject(new Error("[X] This isn't an email address: no '@'")); + + return input.toLowerCase(); + } + + +, onDebounceAsync: function (rs, ws, input, ch) { + // Do a check on the input after 300ms without waiting for the return character + + // return true if the input is complete + + return false; // otherwise the input is not complete + } +, debounceTimeout: 300 // default is 300 +} +``` + +TODO + +``` +, onCharAsync: function (rs, ws, input, ch) { + // the same as debounceTimeout 0 + } +``` + + +Debugging +========= + +### How to detect a pipe + +Your run-of-the-mill bash scripts will not work if you require user input. + +You can check to see if input or output is being handled by a pipe by checking the `isTTY` property. + +* `process.stdin.isTTY` +* `process.stdout.isTTY` + +``` +node example.js +stdin.isTTY: true +stdout.isTTY: true +``` + +``` +node bin/oauth3.js | grep '' +stdin.isTTY: true +stdout.isTTY: undefined +``` + +``` +echo 'hello' | node bin/oauth3.js +stdin.isTTY: undefined +stdout.isTTY: true +``` + +``` +echo 'hello' | node bin/oauth3.js | grep '' +stdin.isTTY: undefined +stdout.isTTY: undefined +``` diff --git a/index.js b/index.js index c7b1f85..d35fd94 100644 --- a/index.js +++ b/index.js @@ -160,7 +160,7 @@ var form = { var debouncer = { set: function () { - if (!cbs.onDebounce) { + if (!(cbs.onDebounceAsync||cbs.onDebounce)) { return; } @@ -172,8 +172,18 @@ var form = { debouncer._timeout = setTimeout(function () { rrs.pause(); - return cbs.onDebounce(ws._input.join(''), ch).then(function () { + return (cbs.onDebounceAsync||cbs.onDebounce)(ws._input.join(''), ch).then(function (result) { rrs.resume(); + + if (!result) { + return; + } + + if (result.complete) { + callback(); + } + + return result.input; }, function (err) { var errmsg = colors.red(err.message); form.setStatus(rrs, ws, errmsg); @@ -182,6 +192,9 @@ var form = { }); }, cbs.debounceTimeout || 300); } + , clear: function () { + clearTimeout(debouncer._timeout); + } }; function callback() { @@ -190,7 +203,7 @@ var form = { rrs.pause(); - cbs.onReturnAsync(rrs, ws, ws._input.join(''), ch).then(function () { + (cbs.onReturnAsync||cbs.onReturn)(rrs, ws, ws._input.join(''), ch).then(function (normalInput) { ws.write('\n'); ws.clearLine(); // person just hit enter, they are on the next line // (and this will clear the status, if any) @@ -200,7 +213,7 @@ var form = { var input = ws._input.join(''); ws._input = []; ws._inputIndex = 0; - resolve({ input: input }); + resolve({ input: normalInput || input }); }, function (err) { rrs.on('data', onData); @@ -214,7 +227,6 @@ var form = { function onData(chunk) { var ch = chunk.toString('ascii'); var x; - debouncer.set(); if (CTRL_C === ch) { console.log(""); @@ -229,12 +241,15 @@ var form = { case LF: case "\n\r": case "\r": + debouncer.clear(); callback(); break; case BKSP: case WIN_BKSP: case ARROW_LEFT: case ARROW_RIGHT: + debouncer.clear(); + // Position-control and delete characters are handled by write wrapper ws.write(ch); break; case ARROW_UP: // TODO history, show pass @@ -245,6 +260,7 @@ var form = { // TODO auto-complete break; default: + debouncer.set(); form.setStatus(rrs, ws, colors.dim( "inputIndex: " + ws._inputIndex + " input:" + ws._input.join('') @@ -300,7 +316,12 @@ var form = { }; var inputs = { - email: { + text: { + onReturnAsync: function (rrs, ws, str) { + return; + } + } +, email: { onReturnAsync: function (rrs, ws, str) { str = str.trim(); var dns = PromiseA.promisifyAll(require('dns')); @@ -347,7 +368,7 @@ var inputs = { form.setStatus(rrs, ws, colors.blue("testing `dig A '" + urlObj.hostname + "'` ... ")); return dns.resolveAsync(urlObj.hostname).then(function () { - return; + return str; }, function () { return PromiseA.reject(new Error("[X] '" + urlObj.hostname + "' doesn't look right (dns lookup failed)")); });