From 6179b603b17571277132fbf29c317bc10ae08c60 Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Tue, 26 Mar 2019 14:34:42 -0600 Subject: [PATCH 01/42] Starting to make the CLI load the file in Node. --- common/lmlayer-cli/index.js | 69 +++++++++++++++++++++++++++++++++ common/lmlayer-cli/package.json | 11 ++++++ 2 files changed, 80 insertions(+) create mode 100644 common/lmlayer-cli/index.js create mode 100644 common/lmlayer-cli/package.json diff --git a/common/lmlayer-cli/index.js b/common/lmlayer-cli/index.js new file mode 100644 index 0000000000..6888f3a626 --- /dev/null +++ b/common/lmlayer-cli/index.js @@ -0,0 +1,69 @@ +/** + * + */ +const LMLayer = require('../predictive-text'); + + +asyncMain().then(_ => process.exit(0)); + +async function asyncMain() { + let lm = new LMLayer({}, createAsyncWorker()); + let config = await lm.loadModel('./example.crk.wordlist_wahkohtowin.model.js'); + let prediction = await lm.predict(insertCharacter('n'), getCurrentContext()); +} + +/** + * A transform that inserts a single character. + */ +function insertText(char) { + return { + insert: char, + deleteLeft: 0 + }; +} + +/** + * Gets the current context from the current buffer. + */ +function getCurrentContext() { + // TODO: base this on an actual buffer! + return { + left: '', + startOfBuffer: true, + endOfBuffer: true, + }; +} + + +function createAsyncWorker() { + // XXX: import the LMLayerWorker directly -- I know where it is built. + const LMLayerWorker = require('../predictive-text/build/intermediate'); + const vm = require('vm'); + const fs = require('fs'); + + let workerScope = { + postMessage(message) { + console.log('[worker]', message); + }, + importScripts(uri) { + let sourceCode = fs.readFileSync(uri, 'UTF-8'); + console.log('[worker] execing file:', uri); + vm.runInContext(sourceCode, workerScope); + } + }; + vm.createContext(workerScope); + + let worker = LMLayerWorker.install(workerScope); + // + LMLayerWorker['loadModel'] = function (uri) { + console.log('Yup, loading this here script...'); + worker.loadModel(uri); + } + + return { + postMessage(message) { + console.log('[top-level]', message); + workerScope.onmessage({ data: message }); + } + }; +} diff --git a/common/lmlayer-cli/package.json b/common/lmlayer-cli/package.json new file mode 100644 index 0000000000..6bbe585db5 --- /dev/null +++ b/common/lmlayer-cli/package.json @@ -0,0 +1,11 @@ +{ + "name": "lmlayer-cli", + "version": "1.0.0", + "description": "Command line interface to the predictive text engine.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Eddie Antonio Santos ", + "license": "MIT" +} From 49478737fa8d1d55ec7c4d719a478b578a4859e5 Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Tue, 26 Mar 2019 16:09:54 -0600 Subject: [PATCH 02/42] Move CLI to common/test/predictive-text/ --- common/{lmlayer-cli => test/predictive-text}/index.js | 4 ++-- common/{lmlayer-cli => test/predictive-text}/package.json | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename common/{lmlayer-cli => test/predictive-text}/index.js (92%) rename common/{lmlayer-cli => test/predictive-text}/package.json (100%) diff --git a/common/lmlayer-cli/index.js b/common/test/predictive-text/index.js similarity index 92% rename from common/lmlayer-cli/index.js rename to common/test/predictive-text/index.js index 6888f3a626..0e755cc5f9 100644 --- a/common/lmlayer-cli/index.js +++ b/common/test/predictive-text/index.js @@ -1,7 +1,7 @@ /** * */ -const LMLayer = require('../predictive-text'); +const LMLayer = require('../../predictive-text'); asyncMain().then(_ => process.exit(0)); @@ -37,7 +37,7 @@ function getCurrentContext() { function createAsyncWorker() { // XXX: import the LMLayerWorker directly -- I know where it is built. - const LMLayerWorker = require('../predictive-text/build/intermediate'); + const LMLayerWorker = require('../../predictive-text/build/intermediate'); const vm = require('vm'); const fs = require('fs'); diff --git a/common/lmlayer-cli/package.json b/common/test/predictive-text/package.json similarity index 100% rename from common/lmlayer-cli/package.json rename to common/test/predictive-text/package.json From f28a657f4181f7094686aed4faf72de06688343e Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Thu, 28 Mar 2019 10:26:34 -0600 Subject: [PATCH 03/42] Things are more work-y... --- common/predictive-text/index.ts | 8 +++---- common/test/predictive-text/index.js | 33 ++++++++++++++++++---------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/common/predictive-text/index.ts b/common/predictive-text/index.ts index 5e9f9779e6..7bafc4ca0e 100644 --- a/common/predictive-text/index.ts +++ b/common/predictive-text/index.ts @@ -90,14 +90,14 @@ namespace com.keyman.text.prediction { */ loadModel(modelFilePath: string): Promise { return new Promise((resolve, _reject) => { + // Sets up so the promise is resolved in the onMessage() callback, when it receives + // the 'ready' message. + this._declareLMLayerReady = resolve; + this._worker.postMessage({ message: 'load', model: modelFilePath }); - - // Sets up so the promise is resolved in the onMessage() callback, when it receives - // the 'ready' message. - this._declareLMLayerReady = resolve; }); } diff --git a/common/test/predictive-text/index.js b/common/test/predictive-text/index.js index 0e755cc5f9..7c8baa7120 100644 --- a/common/test/predictive-text/index.js +++ b/common/test/predictive-text/index.js @@ -41,29 +41,38 @@ function createAsyncWorker() { const vm = require('vm'); const fs = require('fs'); + let worker = { + postMessage(message) { + console.log('[top-level]', message); + workerScope.onmessage({ data: message }); + }, + onmessage(message) { + throw new Error('this should be reassigned!'); + } + }; + let workerScope = { + LMLayerWorker, postMessage(message) { console.log('[worker]', message); + worker.onmessage({ data: message }); }, importScripts(uri) { let sourceCode = fs.readFileSync(uri, 'UTF-8'); console.log('[worker] execing file:', uri); - vm.runInContext(sourceCode, workerScope); + vm.runInContext(sourceCode, workerScope, { + filename: uri, + displayErrors: true, + breakOnSigint: true, + }); } }; vm.createContext(workerScope); - let worker = LMLayerWorker.install(workerScope); - // - LMLayerWorker['loadModel'] = function (uri) { - console.log('Yup, loading this here script...'); - worker.loadModel(uri); + let internal = LMLayerWorker.install(workerScope); + LMLayerWorker.loadModel = function (uri) { + internal.loadModel(uri); } - return { - postMessage(message) { - console.log('[top-level]', message); - workerScope.onmessage({ data: message }); - } - }; + return worker; } From 9b90c5e06ed031a86accd236a259cfa329d3498f Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Thu, 28 Mar 2019 13:41:17 -0600 Subject: [PATCH 04/42] Insert hacky code to make current lexical-model stuff working. --- .../worker/models/wordlist-model.ts | 13 ++++++++++++- common/test/predictive-text/index.js | 18 +++++++++++++++--- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/common/predictive-text/worker/models/wordlist-model.ts b/common/predictive-text/worker/models/wordlist-model.ts index 2f3d564eb3..b2c167b42b 100644 --- a/common/predictive-text/worker/models/wordlist-model.ts +++ b/common/predictive-text/worker/models/wordlist-model.ts @@ -45,7 +45,18 @@ private _wordlist: string[]; constructor(wordlist: string[]) { - this._wordlist = wordlist; + // XXX: This may be given an Array of pairs, where the first element is + // the word and the second is the count. e.g., + // [["elephant", 2], ["zebra", 3], ["octopuses", 13]] + // + // Ignore the count when constructing our wordlist. e.g., + // ["elephant", "zebra", "octopuses"] + if (wordlist.length && Array.isArray(wordlist[0])) { + // @ts-ignore + this._wordlist = wordlist.map(([word, _count]) => word); + } else { + this._wordlist = wordlist; + } } configure(capabilities: Capabilities): Configuration { diff --git a/common/test/predictive-text/index.js b/common/test/predictive-text/index.js index 7c8baa7120..aa5faa43e6 100644 --- a/common/test/predictive-text/index.js +++ b/common/test/predictive-text/index.js @@ -4,7 +4,12 @@ const LMLayer = require('../../predictive-text'); -asyncMain().then(_ => process.exit(0)); +asyncMain() + .then(_ => process.exit(0)) + .catch(err => { + console.error(err); + process.exit(127); + }) async function asyncMain() { let lm = new LMLayer({}, createAsyncWorker()); @@ -15,7 +20,7 @@ async function asyncMain() { /** * A transform that inserts a single character. */ -function insertText(char) { +function insertCharacter(char) { return { insert: char, deleteLeft: 0 @@ -70,7 +75,14 @@ function createAsyncWorker() { vm.createContext(workerScope); let internal = LMLayerWorker.install(workerScope); - LMLayerWorker.loadModel = function (uri) { + + // XXX: The current generated file expects these functions, but they do not, + // and will not exist! Stub them out: + LMLayerWorker['loadWordBreaker'] = function () { + console.warn('ignoring word breaker'); + }; + LMLayerWorker['loadModel'] = function (uri) { + // delegate to the internal LMLayerWorker instance: internal.loadModel(uri); } From dff38ef7a27b6548eb8fa66b912235da9f1d7b41 Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Mon, 1 Apr 2019 14:45:19 -0400 Subject: [PATCH 05/42] Factor out log internal message. --- common/test/predictive-text/index.js | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/common/test/predictive-text/index.js b/common/test/predictive-text/index.js index aa5faa43e6..fca85feeb3 100644 --- a/common/test/predictive-text/index.js +++ b/common/test/predictive-text/index.js @@ -1,5 +1,6 @@ /** - * + * Implements a simple CLI for interactively testing the given predictive text + * model. */ const LMLayer = require('../../predictive-text'); @@ -14,7 +15,16 @@ asyncMain() async function asyncMain() { let lm = new LMLayer({}, createAsyncWorker()); let config = await lm.loadModel('./example.crk.wordlist_wahkohtowin.model.js'); - let prediction = await lm.predict(insertCharacter('n'), getCurrentContext()); + + // TODO: a REPL of sorts here: + // + // > type some text + // [suggestions] [appear] [here] (press tab to accept) + + let suggestions = await lm.predict(insertCharacter('n'), getCurrentContext()); + for (let s of suggestions) { + console.log('>>>', s); + } } /** @@ -48,7 +58,7 @@ function createAsyncWorker() { let worker = { postMessage(message) { - console.log('[top-level]', message); + logInternalWorkerMessage('top-level', message); workerScope.onmessage({ data: message }); }, onmessage(message) { @@ -59,12 +69,12 @@ function createAsyncWorker() { let workerScope = { LMLayerWorker, postMessage(message) { - console.log('[worker]', message); + logInternalWorkerMessage('worker', message); worker.onmessage({ data: message }); }, importScripts(uri) { let sourceCode = fs.readFileSync(uri, 'UTF-8'); - console.log('[worker] execing file:', uri); + logInternalWorkerMessage('worker', 'execing file:', uri); vm.runInContext(sourceCode, workerScope, { filename: uri, displayErrors: true, @@ -88,3 +98,10 @@ function createAsyncWorker() { return worker; } + +/** + * For logging messages that pass between the top-level and the worker. + */ +function logInternalWorkerMessage(role, ...args) { + console.log(`[${role}]`, ...args); +} From 2747c4ab33b2b9960b4ad057ed5f5715db2e918e Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Mon, 1 Apr 2019 14:46:55 -0400 Subject: [PATCH 06/42] Only log messages in debug mode. --- common/test/predictive-text/index.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/common/test/predictive-text/index.js b/common/test/predictive-text/index.js index fca85feeb3..4d4a064dee 100644 --- a/common/test/predictive-text/index.js +++ b/common/test/predictive-text/index.js @@ -4,6 +4,8 @@ */ const LMLayer = require('../../predictive-text'); +const WORKER_DEBUG = false; + asyncMain() .then(_ => process.exit(0)) @@ -103,5 +105,7 @@ function createAsyncWorker() { * For logging messages that pass between the top-level and the worker. */ function logInternalWorkerMessage(role, ...args) { - console.log(`[${role}]`, ...args); + if (WORKER_DEBUG) { + console.log(`[${role}]`, ...args); + } } From 4b65ed22e8bd69896a612b400f3b2c50e6694a9a Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Mon, 1 Apr 2019 14:48:14 -0400 Subject: [PATCH 07/42] What I expect the model to look like. --- .../example.crk.wordlist_wahkohtowin.model.js | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 common/test/predictive-text/example.crk.wordlist_wahkohtowin.model.js diff --git a/common/test/predictive-text/example.crk.wordlist_wahkohtowin.model.js b/common/test/predictive-text/example.crk.wordlist_wahkohtowin.model.js new file mode 100644 index 0000000000..ad14d6f1b4 --- /dev/null +++ b/common/test/predictive-text/example.crk.wordlist_wahkohtowin.model.js @@ -0,0 +1,7 @@ +(function() { +'use strict'; +var model = {}; +model.backingData = [["nimosôm",1],["nohkom",1],["nohtâwiy",1],["nikâwiy",1],["nohcâwîs",1],["nikâwîs",1],["nisis",1],["nisikos",1],["nistês",1],["nimis",1],["nisîmis",1],["nikisos",1],["nitânis",1],["nôsisim",1],["ninâpêm",1],["nitiskwêm",1],["niwîkimâkan",1]]; +LMLayerWorker.loadModel(new models.WordListModel(model.backingData)); +LMLayerWorker.loadWordBreaker(new DefaultWordBreaker({"allowedCharacters":{"initials":"abcdefghijklmnopqrstuvwxyz","medials":"abcdefghijklmnopqrstuvwxyz","finals":"abcdefghijklmnopqrstuvwxyz"},"defaultBreakCharacter":" "})); +})(); From 5d2f60ea1668cad9425b9298471bfac4e38f0fc5 Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Mon, 1 Apr 2019 15:36:10 -0400 Subject: [PATCH 08/42] Get output ANSI codes working. --- common/test/predictive-text/index.js | 31 ++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/common/test/predictive-text/index.js b/common/test/predictive-text/index.js index 4d4a064dee..a665c743cb 100644 --- a/common/test/predictive-text/index.js +++ b/common/test/predictive-text/index.js @@ -6,13 +6,26 @@ const LMLayer = require('../../predictive-text'); const WORKER_DEBUG = false; +/** "Control sequence introducer" for ANSI escape codes: */ +const CSI = '\033['; +/** + * ANSI escape codes to deal with the screen and the cursor. + * See https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences + */ +const ANSI = { + CURSOR_NEXT_LINE: CSI + 'E', // Bring cursor to the beginning of the next line + ERASE_IN_LINE(n=0) { return CSI + n + 'k'; }, // Erase from the cursor to the end of the line + SAVE_CURSOR_POSITION: CSI + 's', // Remembers the current cursor position + RESTORE_CURSOR_POSITION: CSI + 'u', // Moves the cursor to the previously stored position. +}; asyncMain() .then(_ => process.exit(0)) .catch(err => { console.error(err); process.exit(127); - }) + }); + async function asyncMain() { let lm = new LMLayer({}, createAsyncWorker()); @@ -23,10 +36,20 @@ async function asyncMain() { // > type some text // [suggestions] [appear] [here] (press tab to accept) + // Initial line: + process.stdout.write(`> ${ANSI.SAVE_CURSOR_POSITION}`); + + // Proceed to next line to write suggestions. + process.stdout.write(ANSI.CURSOR_NEXT_LINE + ANSI.ERASE_IN_LINE()); + let suggestions = await lm.predict(insertCharacter('n'), getCurrentContext()); - for (let s of suggestions) { - console.log('>>>', s); - } + + // Format the displayed suggestions. + let names = Array.from(suggestions) + .map(({displayAs}) => `[ ${displayAs} ]`) + .join(' '); + process.stdout.write(names); + process.stdout.write(ANSI.RESTORE_CURSOR_POSITION); } /** From 17ae94b6f69a014d1ea1c3dac849d7c92ca3c3d4 Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Mon, 1 Apr 2019 15:47:41 -0400 Subject: [PATCH 09/42] Get current suggestion to be inverted. --- common/test/predictive-text/index.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/common/test/predictive-text/index.js b/common/test/predictive-text/index.js index a665c743cb..68887af101 100644 --- a/common/test/predictive-text/index.js +++ b/common/test/predictive-text/index.js @@ -13,10 +13,14 @@ const CSI = '\033['; * See https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences */ const ANSI = { - CURSOR_NEXT_LINE: CSI + 'E', // Bring cursor to the beginning of the next line - ERASE_IN_LINE(n=0) { return CSI + n + 'k'; }, // Erase from the cursor to the end of the line - SAVE_CURSOR_POSITION: CSI + 's', // Remembers the current cursor position + CURSOR_NEXT_LINE: CSI + 'E', // Bring cursor to the beginning of the next line. + ERASE_IN_LINE(n=0) { return CSI + n + 'k'; }, // Erase from the cursor to the end of the line. + SAVE_CURSOR_POSITION: CSI + 's', // Remembers the current cursor position. RESTORE_CURSOR_POSITION: CSI + 'u', // Moves the cursor to the previously stored position. + BOLD: CSI + '1m', // Set bold text. + REVERSE_VIDEO: CSI + '7m', // Invert the text colour (swap background and foreground colours). + NORMAL_VIDEO: CSI + '27m', // Uninvert the text colour (swap background and foreground colours). + NORMAL: CSI + 'm', // Set all graphics renditions attributes off. }; asyncMain() @@ -45,9 +49,14 @@ async function asyncMain() { let suggestions = await lm.predict(insertCharacter('n'), getCurrentContext()); // Format the displayed suggestions. + let selected = 0; let names = Array.from(suggestions) - .map(({displayAs}) => `[ ${displayAs} ]`) - .join(' '); + .map(({displayAs}, index) => { + if (index === selected) { + return `[${ANSI.REVERSE_VIDEO}${displayAs}${ANSI.NORMAL_VIDEO}]`; + } + return `[ ${displayAs} ]` + }).join(' '); process.stdout.write(names); process.stdout.write(ANSI.RESTORE_CURSOR_POSITION); } From 6257fe5c0b8e2db1bbff19bde1cfd554cc9f96fd Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Mon, 1 Apr 2019 16:01:28 -0400 Subject: [PATCH 10/42] Factor out render suggestions. --- common/test/predictive-text/index.js | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/common/test/predictive-text/index.js b/common/test/predictive-text/index.js index 68887af101..7aa749af67 100644 --- a/common/test/predictive-text/index.js +++ b/common/test/predictive-text/index.js @@ -43,22 +43,30 @@ async function asyncMain() { // Initial line: process.stdout.write(`> ${ANSI.SAVE_CURSOR_POSITION}`); - // Proceed to next line to write suggestions. + + let suggestions = Array.from(await lm.predict(insertCharacter('n'), getCurrentContext())); + renderSuggestions(suggestions, 1); +} + +/** + * Render the given suggestions on the next line after the cursor. + */ +function renderSuggestions(suggestions, selected) { + // Wherever we are, save the current cursor position. + process.stdout.write(`${ANSI.SAVE_CURSOR_POSITION}`); + + // Jump to next line and erase it to write suggestions. process.stdout.write(ANSI.CURSOR_NEXT_LINE + ANSI.ERASE_IN_LINE()); - let suggestions = await lm.predict(insertCharacter('n'), getCurrentContext()); - // Format the displayed suggestions. - let selected = 0; - let names = Array.from(suggestions) + let line = suggestions .map(({displayAs}, index) => { if (index === selected) { - return `[${ANSI.REVERSE_VIDEO}${displayAs}${ANSI.NORMAL_VIDEO}]`; + return `${ANSI.REVERSE_VIDEO}[${displayAs}]${ANSI.NORMAL_VIDEO}`; } return `[ ${displayAs} ]` }).join(' '); - process.stdout.write(names); - process.stdout.write(ANSI.RESTORE_CURSOR_POSITION); + process.stdout.write(line + ANSI.RESTORE_CURSOR_POSITION); } /** From 2913f6feaf3fb0cb94dad6b84b87a14900c60c3b Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Wed, 3 Apr 2019 19:36:12 -0400 Subject: [PATCH 11/42] Check that we're being run from an interactive terminal. --- common/test/predictive-text/index.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/common/test/predictive-text/index.js b/common/test/predictive-text/index.js index 7aa749af67..c3a3074a1b 100644 --- a/common/test/predictive-text/index.js +++ b/common/test/predictive-text/index.js @@ -32,6 +32,12 @@ asyncMain() async function asyncMain() { + // Ensure we're running in the terminal + if (!process.stdin.isTTY) { + throw new Error('must be run from interactive terminal'); + } + + // Load the LMLayer and the desired model. let lm = new LMLayer({}, createAsyncWorker()); let config = await lm.loadModel('./example.crk.wordlist_wahkohtowin.model.js'); From 8d4e96d651c8ac79ff33ded9f651b96face557eb Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Wed, 3 Apr 2019 19:48:40 -0400 Subject: [PATCH 12/42] Trying to figure out Node's readline module... --- common/test/predictive-text/index.js | 89 +++++++++++++++++++++++++--- 1 file changed, 81 insertions(+), 8 deletions(-) diff --git a/common/test/predictive-text/index.js b/common/test/predictive-text/index.js index c3a3074a1b..5507c046df 100644 --- a/common/test/predictive-text/index.js +++ b/common/test/predictive-text/index.js @@ -2,6 +2,10 @@ * Implements a simple CLI for interactively testing the given predictive text * model. */ + +const readline = require('readline'); + +// Load the most recent LMLayer code locally. const LMLayer = require('../../predictive-text'); const WORKER_DEBUG = false; @@ -23,20 +27,89 @@ const ANSI = { NORMAL: CSI + 'm', // Set all graphics renditions attributes off. }; -asyncMain() - .then(_ => process.exit(0)) - .catch(err => { - console.error(err); - process.exit(127); - }); +// Do it! +main(); - -async function asyncMain() { +function main() { // Ensure we're running in the terminal if (!process.stdin.isTTY) { throw new Error('must be run from interactive terminal'); } + readline.emitKeypressEvents(process.stdin); + if (process.stdin.isTTY) + process.stdin.setRawMode(true); + process.stdin.resume(); + + /** + * + * [ 'n', + * { sequence: 'n', + * name: 'n', + * ctrl: false, + * meta: false, + * shift: false } ] + *[ 'o', + * { sequence: 'o', + * name: 'o', + * ctrl: false, + * meta: false, + * shift: false } ] + *[ 'O', + * { sequence: 'O', name: 'o', ctrl: false, meta: false, shift: true } ] + *[ '\t', + * { sequence: '\t', + * name: 'tab', + * ctrl: false, + * meta: false, + * shift: false } ] + *[ '\r', + * { sequence: '\r', + * name: 'return', + * ctrl: false, + * meta: false, + * shift: false } ] + *[ undefined, + * { sequence: '\u001b[C', + * name: 'right', + * ctrl: false, + * meta: false, + * shift: false, + * code: '[C' } ] + *[ '\u0004', + * { sequence: '\u0004', + * name: 'd', + * ctrl: true, + * meta: false, + * shift: false } ] + *[ '\u0003', + * { sequence: '\u0003', + * name: 'c', + * ctrl: true, + * meta: false, + * shift: false } ] + * + */ + process.stdin.on('keypress', function (char, keypress) { + console.log({char, keypress}); + if (keypress.sequence === '\u0003' || keypress.sequence === '\u0004') { + process.exit(0); + } + }); + + return; + + asyncMain() + .then(_ => process.exit(0)) + .catch(err => { + console.error(err); + process.exit(127); + }); +} + + +async function asyncMain() { + // Load the LMLayer and the desired model. let lm = new LMLayer({}, createAsyncWorker()); let config = await lm.loadModel('./example.crk.wordlist_wahkohtowin.model.js'); From abe0ba754a40f57c3f0513ffd131c7481b84185a Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Thu, 11 Apr 2019 13:49:21 -0600 Subject: [PATCH 13/42] Factor out setupStdin(). --- common/test/predictive-text/index.js | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/common/test/predictive-text/index.js b/common/test/predictive-text/index.js index 5507c046df..4d48420bd3 100644 --- a/common/test/predictive-text/index.js +++ b/common/test/predictive-text/index.js @@ -5,6 +5,9 @@ const readline = require('readline'); +const {EventIterator} = require('event-iterator'); + + // Load the most recent LMLayer code locally. const LMLayer = require('../../predictive-text'); @@ -36,10 +39,7 @@ function main() { throw new Error('must be run from interactive terminal'); } - readline.emitKeypressEvents(process.stdin); - if (process.stdin.isTTY) - process.stdin.setRawMode(true); - process.stdin.resume(); + let stream = setupStdin(); /** * @@ -90,7 +90,7 @@ function main() { * shift: false } ] * */ - process.stdin.on('keypress', function (char, keypress) { + stream.on('keypress', function (char, keypress) { console.log({char, keypress}); if (keypress.sequence === '\u0003' || keypress.sequence === '\u0004') { process.exit(0); @@ -171,6 +171,19 @@ function getCurrentContext() { } +/** + * Setup stdin for reading keypress events. + */ +function setupStdin() { + readline.emitKeypressEvents(process.stdin); + if (process.stdin.isTTY) + process.stdin.setRawMode(true); + process.stdin.resume(); + + return process.stdin; +} + + function createAsyncWorker() { // XXX: import the LMLayerWorker directly -- I know where it is built. const LMLayerWorker = require('../../predictive-text/build/intermediate'); From 8f7305c77e21b60fa8a5630f3d72e389aefb9eea Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Thu, 11 Apr 2019 16:17:54 -0600 Subject: [PATCH 14/42] Works as an event stream now! --- common/test/predictive-text/.gitignore | 1 + common/test/predictive-text/index.js | 27 ++++++++++++++----- common/test/predictive-text/package-lock.json | 13 +++++++++ common/test/predictive-text/package.json | 5 +++- 4 files changed, 38 insertions(+), 8 deletions(-) create mode 100644 common/test/predictive-text/.gitignore create mode 100644 common/test/predictive-text/package-lock.json diff --git a/common/test/predictive-text/.gitignore b/common/test/predictive-text/.gitignore new file mode 100644 index 0000000000..c2658d7d1b --- /dev/null +++ b/common/test/predictive-text/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/common/test/predictive-text/index.js b/common/test/predictive-text/index.js index 4d48420bd3..bbb8a0f072 100644 --- a/common/test/predictive-text/index.js +++ b/common/test/predictive-text/index.js @@ -90,14 +90,27 @@ function main() { * shift: false } ] * */ - stream.on('keypress', function (char, keypress) { - console.log({char, keypress}); - if (keypress.sequence === '\u0003' || keypress.sequence === '\u0004') { - process.exit(0); - } - }); - return; + function keypressOf(stream) { + return new EventIterator( + (push, stop, fail) => { + stream.on('keypress', (char, keypress) => push([char, keypress])); + stream.on('close', stop); + stream.on('error', fail); + } + ); + } + + async function derp() { + for await (let [char, keypress] of keypressOf(stream)) { + console.log({char, keypress}); + if (keypress.sequence === '\u0003' || keypress.sequence === '\u0004') { + process.exit(0); + } + } + } + + return derp().catch(_ => process.exit(127)); asyncMain() .then(_ => process.exit(0)) diff --git a/common/test/predictive-text/package-lock.json b/common/test/predictive-text/package-lock.json new file mode 100644 index 0000000000..cb5856d59d --- /dev/null +++ b/common/test/predictive-text/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "lmlayer-cli", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "event-iterator": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/event-iterator/-/event-iterator-1.2.0.tgz", + "integrity": "sha512-Daq7YUl0Mv1i4QEgzGQlz0jrx7hUFNyLGbiF+Ap7NCMCjDLCCnolyj6s0TAc6HmrBziO5rNVHsPwGMp7KdRPvw==" + } + } +} diff --git a/common/test/predictive-text/package.json b/common/test/predictive-text/package.json index 6bbe585db5..bd3e496037 100644 --- a/common/test/predictive-text/package.json +++ b/common/test/predictive-text/package.json @@ -7,5 +7,8 @@ "test": "echo \"Error: no test specified\" && exit 1" }, "author": "Eddie Antonio Santos ", - "license": "MIT" + "license": "MIT", + "dependencies": { + "event-iterator": "^1.2.0" + } } From 6ae6027b2afebd5e146f1ca4435fafea7da2aa2b Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Fri, 12 Apr 2019 09:15:54 -0600 Subject: [PATCH 15/42] Add a basic README. --- common/test/predictive-text/README.md | 40 +++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 common/test/predictive-text/README.md diff --git a/common/test/predictive-text/README.md b/common/test/predictive-text/README.md new file mode 100644 index 0000000000..a1ef5076ea --- /dev/null +++ b/common/test/predictive-text/README.md @@ -0,0 +1,40 @@ +Predictive Text testing CLI +--------------------------- + +Provides a command line interface to try out predictive text models. + +Install +------- + +**NOTE**: Requires Node >= 10.0 + +Install dependencies with `npm`: + + npm install + +Usage +----- + +Start it like so: + + $ node . + +You will be presented with a prompt. Type any phrase in the model's +language. Press Tab to start choosing a prediction. Press the +left and right arrow keys to go through suggestions. Press +Enter to select the given suggestion. Press Esc. + +Press Ctrl+D or Ctrl+C to quit. + + > He + [ Hello ] [ Hey ] [ He he ] + + +ROADMAP +------- + + * The command line interface! + * \-f _filename_ to load a model from a file + * `LMPATH` environment variable to load a named model by id: e.g,. + `node . example.crk.wordlist` will load the given model, + provided `LMPATH` is defined. From 74ca1ecd3be3cba08ec76f9dc396e54ceda870a6 Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Fri, 12 Apr 2019 09:25:34 -0600 Subject: [PATCH 16/42] Clean up keypressOf() function. --- common/test/predictive-text/index.js | 84 ++++++++-------------------- 1 file changed, 23 insertions(+), 61 deletions(-) diff --git a/common/test/predictive-text/index.js b/common/test/predictive-text/index.js index bbb8a0f072..72c922f793 100644 --- a/common/test/predictive-text/index.js +++ b/common/test/predictive-text/index.js @@ -41,66 +41,6 @@ function main() { let stream = setupStdin(); - /** - * - * [ 'n', - * { sequence: 'n', - * name: 'n', - * ctrl: false, - * meta: false, - * shift: false } ] - *[ 'o', - * { sequence: 'o', - * name: 'o', - * ctrl: false, - * meta: false, - * shift: false } ] - *[ 'O', - * { sequence: 'O', name: 'o', ctrl: false, meta: false, shift: true } ] - *[ '\t', - * { sequence: '\t', - * name: 'tab', - * ctrl: false, - * meta: false, - * shift: false } ] - *[ '\r', - * { sequence: '\r', - * name: 'return', - * ctrl: false, - * meta: false, - * shift: false } ] - *[ undefined, - * { sequence: '\u001b[C', - * name: 'right', - * ctrl: false, - * meta: false, - * shift: false, - * code: '[C' } ] - *[ '\u0004', - * { sequence: '\u0004', - * name: 'd', - * ctrl: true, - * meta: false, - * shift: false } ] - *[ '\u0003', - * { sequence: '\u0003', - * name: 'c', - * ctrl: true, - * meta: false, - * shift: false } ] - * - */ - - function keypressOf(stream) { - return new EventIterator( - (push, stop, fail) => { - stream.on('keypress', (char, keypress) => push([char, keypress])); - stream.on('close', stop); - stream.on('error', fail); - } - ); - } - async function derp() { for await (let [char, keypress] of keypressOf(stream)) { console.log({char, keypress}); @@ -122,7 +62,6 @@ function main() { async function asyncMain() { - // Load the LMLayer and the desired model. let lm = new LMLayer({}, createAsyncWorker()); let config = await lm.loadModel('./example.crk.wordlist_wahkohtowin.model.js'); @@ -254,3 +193,26 @@ function logInternalWorkerMessage(role, ...args) { console.log(`[${role}]`, ...args); } } + +/** + * Allows you to iterate over each keypress event from the given stream. + * + * Each iteration will yield a two-valued array of [char, keypress]. + * `char` is the string emitted by the keypress. This MAY be undefined! + * `keypress` is an object with the following properties: + * + * sequence: string; // the raw sequence recieved by the terminal driver. + * name: string; // name of the primary key pressed + * ctrl: bool; // was ctrl pressed? + * meta: bool; // was meta (Command/Windows key) pressed? + * shift: bool; // was shift pressed? + */ +function keypressOf(stream) { + return new EventIterator( + (push, stop, fail) => { + stream.on('keypress', (char, keypress) => push([char, keypress])); + stream.on('close', stop); + stream.on('error', fail); + } + ); +} From 81747006562f4023552bfe4bf7addc6e7b8cfd22 Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Fri, 12 Apr 2019 09:28:04 -0600 Subject: [PATCH 17/42] Combine setupStdin() with keypressesFromStdin(). --- common/test/predictive-text/index.js | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/common/test/predictive-text/index.js b/common/test/predictive-text/index.js index 72c922f793..08857d67ee 100644 --- a/common/test/predictive-text/index.js +++ b/common/test/predictive-text/index.js @@ -39,10 +39,8 @@ function main() { throw new Error('must be run from interactive terminal'); } - let stream = setupStdin(); - async function derp() { - for await (let [char, keypress] of keypressOf(stream)) { + for await (let [char, keypress] of keypressesFromStdin()) { console.log({char, keypress}); if (keypress.sequence === '\u0003' || keypress.sequence === '\u0004') { process.exit(0); @@ -50,9 +48,7 @@ function main() { } } - return derp().catch(_ => process.exit(127)); - - asyncMain() + return derp() .then(_ => process.exit(0)) .catch(err => { console.error(err); @@ -127,12 +123,6 @@ function getCurrentContext() { * Setup stdin for reading keypress events. */ function setupStdin() { - readline.emitKeypressEvents(process.stdin); - if (process.stdin.isTTY) - process.stdin.setRawMode(true); - process.stdin.resume(); - - return process.stdin; } @@ -195,7 +185,7 @@ function logInternalWorkerMessage(role, ...args) { } /** - * Allows you to iterate over each keypress event from the given stream. + * Allows you to iterate over each keypress event from stdin. * * Each iteration will yield a two-valued array of [char, keypress]. * `char` is the string emitted by the keypress. This MAY be undefined! @@ -207,7 +197,15 @@ function logInternalWorkerMessage(role, ...args) { * meta: bool; // was meta (Command/Windows key) pressed? * shift: bool; // was shift pressed? */ -function keypressOf(stream) { +function keypressesFromStdin() { + let stream = process.stdin; + + readline.emitKeypressEvents(stream); + if (stream.isTTY) { + stream.setRawMode(true); + } + stream.resume(); + return new EventIterator( (push, stop, fail) => { stream.on('keypress', (char, keypress) => push([char, keypress])); From 1915a7635fee68603458bf918cb1e3106a6ef5c7 Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Fri, 12 Apr 2019 09:48:28 -0600 Subject: [PATCH 18/42] It sort of works now! --- common/test/predictive-text/index.js | 57 +++++++++++++++------------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/common/test/predictive-text/index.js b/common/test/predictive-text/index.js index 08857d67ee..6182d0e941 100644 --- a/common/test/predictive-text/index.js +++ b/common/test/predictive-text/index.js @@ -21,7 +21,7 @@ const CSI = '\033['; */ const ANSI = { CURSOR_NEXT_LINE: CSI + 'E', // Bring cursor to the beginning of the next line. - ERASE_IN_LINE(n=0) { return CSI + n + 'k'; }, // Erase from the cursor to the end of the line. + ERASE_IN_LINE(n=2) { return CSI + n + 'K'; }, // Erase the entire line. SAVE_CURSOR_POSITION: CSI + 's', // Remembers the current cursor position. RESTORE_CURSOR_POSITION: CSI + 'u', // Moves the cursor to the previously stored position. BOLD: CSI + '1m', // Set bold text. @@ -39,16 +39,7 @@ function main() { throw new Error('must be run from interactive terminal'); } - async function derp() { - for await (let [char, keypress] of keypressesFromStdin()) { - console.log({char, keypress}); - if (keypress.sequence === '\u0003' || keypress.sequence === '\u0004') { - process.exit(0); - } - } - } - - return derp() + asyncMain() .then(_ => process.exit(0)) .catch(err => { console.error(err); @@ -62,17 +53,23 @@ async function asyncMain() { let lm = new LMLayer({}, createAsyncWorker()); let config = await lm.loadModel('./example.crk.wordlist_wahkohtowin.model.js'); - // TODO: a REPL of sorts here: - // + // Setup the REPL // > type some text // [suggestions] [appear] [here] (press tab to accept) // Initial line: - process.stdout.write(`> ${ANSI.SAVE_CURSOR_POSITION}`); + process.stdout.write(`> `); - - let suggestions = Array.from(await lm.predict(insertCharacter('n'), getCurrentContext())); - renderSuggestions(suggestions, 1); + for await (let [char, keypress] of keypressesFromStdin()) { + if (keypress.sequence === '\u0003' || keypress.sequence === '\u0004') { + process.stdout.write('\n'); + process.exit(0); + } else { + process.stdout.write(char); + let suggestions = Array.from(await lm.predict(insertCharacter(char), getCurrentContext())); + renderSuggestions(suggestions, 0); + } + } } /** @@ -80,20 +77,26 @@ async function asyncMain() { */ function renderSuggestions(suggestions, selected) { // Wherever we are, save the current cursor position. - process.stdout.write(`${ANSI.SAVE_CURSOR_POSITION}`); + process.stdout.write(ANSI.SAVE_CURSOR_POSITION); // Jump to next line and erase it to write suggestions. process.stdout.write(ANSI.CURSOR_NEXT_LINE + ANSI.ERASE_IN_LINE()); - // Format the displayed suggestions. - let line = suggestions - .map(({displayAs}, index) => { - if (index === selected) { - return `${ANSI.REVERSE_VIDEO}[${displayAs}]${ANSI.NORMAL_VIDEO}`; - } - return `[ ${displayAs} ]` - }).join(' '); - process.stdout.write(line + ANSI.RESTORE_CURSOR_POSITION); + if (!suggestions || suggestions.length === 0) { + process.stdout.write(' no suggestions '); + } else { + // Format the displayed suggestions. + let line = suggestions + .map(({displayAs}, index) => { + if (index === selected) { + return `${ANSI.REVERSE_VIDEO}[${displayAs}]${ANSI.NORMAL_VIDEO}`; + } + return `[ ${displayAs} ]` + }).join(' '); + process.stdout.write(line); + } + + process.stdout.write(ANSI.RESTORE_CURSOR_POSITION); } /** From 909497870f2d51bd058d80ca44af8a576ef53d31 Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Fri, 12 Apr 2019 10:00:42 -0600 Subject: [PATCH 19/42] Factor out quiting logic into wantsQuit(). --- common/test/predictive-text/index.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/common/test/predictive-text/index.js b/common/test/predictive-text/index.js index 6182d0e941..3b3228eb3e 100644 --- a/common/test/predictive-text/index.js +++ b/common/test/predictive-text/index.js @@ -61,7 +61,7 @@ async function asyncMain() { process.stdout.write(`> `); for await (let [char, keypress] of keypressesFromStdin()) { - if (keypress.sequence === '\u0003' || keypress.sequence === '\u0004') { + if (wantsQuit(keypress)) { process.stdout.write('\n'); process.exit(0); } else { @@ -121,11 +121,15 @@ function getCurrentContext() { }; } - /** - * Setup stdin for reading keypress events. + * True if the keypress looks like something that typically quits the program. */ -function setupStdin() { +function wantsQuit(keypress) { + const CTRL_C = '\u0003'; + const CTRL_D = '\u0004'; + const CTRL_BACKSLASH = '\u001C'; + let code = keypress.sequence; + return code === CTRL_C || code === CTRL_D || code === CTRL_BACKSLASH; } From af52f72bb95d2bc73ce5ff77927eab0f3b09ac20 Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Mon, 15 Apr 2019 11:07:13 -0600 Subject: [PATCH 20/42] Remove hardcoded model. --- .../example.crk.wordlist_wahkohtowin.model.js | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 common/test/predictive-text/example.crk.wordlist_wahkohtowin.model.js diff --git a/common/test/predictive-text/example.crk.wordlist_wahkohtowin.model.js b/common/test/predictive-text/example.crk.wordlist_wahkohtowin.model.js deleted file mode 100644 index ad14d6f1b4..0000000000 --- a/common/test/predictive-text/example.crk.wordlist_wahkohtowin.model.js +++ /dev/null @@ -1,7 +0,0 @@ -(function() { -'use strict'; -var model = {}; -model.backingData = [["nimosôm",1],["nohkom",1],["nohtâwiy",1],["nikâwiy",1],["nohcâwîs",1],["nikâwîs",1],["nisis",1],["nisikos",1],["nistês",1],["nimis",1],["nisîmis",1],["nikisos",1],["nitânis",1],["nôsisim",1],["ninâpêm",1],["nitiskwêm",1],["niwîkimâkan",1]]; -LMLayerWorker.loadModel(new models.WordListModel(model.backingData)); -LMLayerWorker.loadWordBreaker(new DefaultWordBreaker({"allowedCharacters":{"initials":"abcdefghijklmnopqrstuvwxyz","medials":"abcdefghijklmnopqrstuvwxyz","finals":"abcdefghijklmnopqrstuvwxyz"},"defaultBreakCharacter":" "})); -})(); From 6f0661d96d42e4937ff4e6c465e17cc3daae148a Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Mon, 15 Apr 2019 11:07:24 -0600 Subject: [PATCH 21/42] Add command line parsing. --- common/test/predictive-text/index.js | 63 +++++++++++++++++-- common/test/predictive-text/package-lock.json | 5 ++ common/test/predictive-text/package.json | 3 +- 3 files changed, 66 insertions(+), 5 deletions(-) diff --git a/common/test/predictive-text/index.js b/common/test/predictive-text/index.js index 3b3228eb3e..aa92206628 100644 --- a/common/test/predictive-text/index.js +++ b/common/test/predictive-text/index.js @@ -6,12 +6,17 @@ const readline = require('readline'); const {EventIterator} = require('event-iterator'); +const program = require('commander'); // Load the most recent LMLayer code locally. const LMLayer = require('../../predictive-text'); + +///////////////////////////////// Constants ///////////////////////////////// + const WORKER_DEBUG = false; +const EXIT_USAGE = 1; /** "Control sequence introducer" for ANSI escape codes: */ const CSI = '\033['; @@ -30,16 +35,46 @@ const ANSI = { NORMAL: CSI + 'm', // Set all graphics renditions attributes off. }; -// Do it! + +//////////////////////////////////// main //////////////////////////////////// + main(); function main() { + // Command line options: + program + .version(require('./package.json').version) + .usage('-f ') + .description('CLI for trying lexical models.') + .option('-f, --model-file ', 'path to model file') + .parse(process.argv); + + let modelFile = program.modelFile; + if (! modelFile) { + console.error(`${program.name()}: You forgot to specify a model!`); + program.outputHelp(); + process.exit(EXIT_USAGE); + } + // Ensure we're running in the terminal if (!process.stdin.isTTY) { throw new Error('must be run from interactive terminal'); } - asyncMain() + // Show a quick "how-to" message. + console.log(unindent(` + # Testing ${modelFile} + # + # Type text in the model's language. Suggestions will appear automatically. + # + # * Press ${ANSI.BOLD}${ANSI.NORMAL} to select a suggestion. + # * Press ${ANSI.BOLD}${ANSI.NORMAL} again to select the next suggestion. + # * Press ${ANSI.BOLD}${ANSI.NORMAL}+${ANSI.BOLD}${ANSI.NORMAL} to select the previous suggestion. + # * Press ${ANSI.BOLD}${ANSI.NORMAL} to accept the suggestion. + # * Press ${ANSI.BOLD}${ANSI.NORMAL}+${ANSI.BOLD}${ANSI.NORMAL} to quit. + `)) + + asyncMain(modelFile) .then(_ => process.exit(0)) .catch(err => { console.error(err); @@ -48,10 +83,10 @@ function main() { } -async function asyncMain() { +async function asyncMain(modelFile) { // Load the LMLayer and the desired model. let lm = new LMLayer({}, createAsyncWorker()); - let config = await lm.loadModel('./example.crk.wordlist_wahkohtowin.model.js'); + let config = await lm.loadModel(modelFile); // Setup the REPL // > type some text @@ -221,3 +256,23 @@ function keypressesFromStdin() { } ); } + +///////////////////////////////// Utilities ///////////////////////////////// + +/** + * Unindents a template literal. + */ +function unindent(string) { + let lines = string.split('\n'); + // Remove empty lines from the top and bottom. + if (lines[0] === '') { + lines.shift(); + } + if (lines[lines.length -1] === '') { + lines.pop(); + } + + let numLeadingSpaces = lines[0].match(/^ */)[0].length; + + return lines.map(line => line.substr(numLeadingSpaces)).join('\n'); +} diff --git a/common/test/predictive-text/package-lock.json b/common/test/predictive-text/package-lock.json index cb5856d59d..4e29d81798 100644 --- a/common/test/predictive-text/package-lock.json +++ b/common/test/predictive-text/package-lock.json @@ -4,6 +4,11 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "commander": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==" + }, "event-iterator": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/event-iterator/-/event-iterator-1.2.0.tgz", diff --git a/common/test/predictive-text/package.json b/common/test/predictive-text/package.json index bd3e496037..ca4fb51d81 100644 --- a/common/test/predictive-text/package.json +++ b/common/test/predictive-text/package.json @@ -1,6 +1,6 @@ { "name": "lmlayer-cli", - "version": "1.0.0", + "version": "0.1.0", "description": "Command line interface to the predictive text engine.", "main": "index.js", "scripts": { @@ -9,6 +9,7 @@ "author": "Eddie Antonio Santos ", "license": "MIT", "dependencies": { + "commander": "^2.20.0", "event-iterator": "^1.2.0" } } From b2b9881d601929d98ab15feb7775f7ef8b3f6c0e Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Mon, 15 Apr 2019 11:10:54 -0600 Subject: [PATCH 22/42] Reorder utility functions. --- common/test/predictive-text/index.js | 82 ++++++++++++++-------------- 1 file changed, 42 insertions(+), 40 deletions(-) diff --git a/common/test/predictive-text/index.js b/common/test/predictive-text/index.js index aa92206628..54c0ce6977 100644 --- a/common/test/predictive-text/index.js +++ b/common/test/predictive-text/index.js @@ -50,7 +50,7 @@ function main() { .parse(process.argv); let modelFile = program.modelFile; - if (! modelFile) { + if (!modelFile) { console.error(`${program.name()}: You forgot to specify a model!`); program.outputHelp(); process.exit(EXIT_USAGE); @@ -134,39 +134,6 @@ function renderSuggestions(suggestions, selected) { process.stdout.write(ANSI.RESTORE_CURSOR_POSITION); } -/** - * A transform that inserts a single character. - */ -function insertCharacter(char) { - return { - insert: char, - deleteLeft: 0 - }; -} - -/** - * Gets the current context from the current buffer. - */ -function getCurrentContext() { - // TODO: base this on an actual buffer! - return { - left: '', - startOfBuffer: true, - endOfBuffer: true, - }; -} - -/** - * True if the keypress looks like something that typically quits the program. - */ -function wantsQuit(keypress) { - const CTRL_C = '\u0003'; - const CTRL_D = '\u0004'; - const CTRL_BACKSLASH = '\u001C'; - let code = keypress.sequence; - return code === CTRL_C || code === CTRL_D || code === CTRL_BACKSLASH; -} - function createAsyncWorker() { // XXX: import the LMLayerWorker directly -- I know where it is built. @@ -217,13 +184,29 @@ function createAsyncWorker() { return worker; } + +///////////////////////////////// Utilities ///////////////////////////////// + /** - * For logging messages that pass between the top-level and the worker. + * Gets the current context from the current buffer. */ -function logInternalWorkerMessage(role, ...args) { - if (WORKER_DEBUG) { - console.log(`[${role}]`, ...args); - } +function getCurrentContext() { + // TODO: base this on an actual buffer! + return { + left: '', + startOfBuffer: true, + endOfBuffer: true, + }; +} + +/** + * A transform that inserts a single character. + */ +function insertCharacter(char) { + return { + insert: char, + deleteLeft: 0 + }; } /** @@ -257,7 +240,15 @@ function keypressesFromStdin() { ); } -///////////////////////////////// Utilities ///////////////////////////////// +/** + * Loges messages that pass between the top-level and the worker, only when + * WORKER_DEBUG is enabled. + */ +function logInternalWorkerMessage(role, ...args) { + if (WORKER_DEBUG) { + console.log(`[${role}]`, ...args); + } +} /** * Unindents a template literal. @@ -276,3 +267,14 @@ function unindent(string) { return lines.map(line => line.substr(numLeadingSpaces)).join('\n'); } + +/** + * True if the keypress looks like something that typically quits the program. + */ +function wantsQuit(keypress) { + const CTRL_C = '\u0003'; + const CTRL_D = '\u0004'; + const CTRL_BACKSLASH = '\u001C'; + let code = keypress.sequence; + return code === CTRL_C || code === CTRL_D || code === CTRL_BACKSLASH; +} From 52cadb537c7ae5448057eb27a0e1ca03abc932e8 Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Mon, 15 Apr 2019 11:35:06 -0600 Subject: [PATCH 23/42] Implement loading model from LMPATH environment variable. --- common/test/predictive-text/index.js | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/common/test/predictive-text/index.js b/common/test/predictive-text/index.js index 54c0ce6977..47e4e504d3 100644 --- a/common/test/predictive-text/index.js +++ b/common/test/predictive-text/index.js @@ -3,12 +3,12 @@ * model. */ +const path = require('path'); const readline = require('readline'); const {EventIterator} = require('event-iterator'); const program = require('commander'); - // Load the most recent LMLayer code locally. const LMLayer = require('../../predictive-text'); @@ -44,13 +44,30 @@ function main() { // Command line options: program .version(require('./package.json').version) - .usage('-f ') + .usage('(-f | )') .description('CLI for trying lexical models.') + .arguments('[model-id]') .option('-f, --model-file ', 'path to model file') .parse(process.argv); - let modelFile = program.modelFile; - if (!modelFile) { + let modelFile; + if (program.modelFile) { + modelFile = program.modelFile; + } else if (program.args[0]) { + // Find the model file under the LMPath. + let modelID = program.args[0]; + let lmPath = process.env['LMPATH']; + let [author, bcp47, uniq] = modelID.split('.'); + + let LMPATH = process.env['LMPATH']; + if (!LMPATH) { + console.error(`${program.name()}: Environment variable LMPATH undefined!`); + program.outputHelp(); + process.exit(EXIT_USAGE); + } + + modelFile = path.join(LMPATH, author, `${bcp47}.${uniq}`, 'build', `${modelID}.model.js`); + } else{ console.error(`${program.name()}: You forgot to specify a model!`); program.outputHelp(); process.exit(EXIT_USAGE); From 94e9727de943531c8196a45c4136212ea3baad5b Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Mon, 15 Apr 2019 11:40:03 -0600 Subject: [PATCH 24/42] Factor out determineModelFile(). --- common/test/predictive-text/index.js | 53 ++++++++++++++++------------ 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/common/test/predictive-text/index.js b/common/test/predictive-text/index.js index 47e4e504d3..ae63095280 100644 --- a/common/test/predictive-text/index.js +++ b/common/test/predictive-text/index.js @@ -50,28 +50,7 @@ function main() { .option('-f, --model-file ', 'path to model file') .parse(process.argv); - let modelFile; - if (program.modelFile) { - modelFile = program.modelFile; - } else if (program.args[0]) { - // Find the model file under the LMPath. - let modelID = program.args[0]; - let lmPath = process.env['LMPATH']; - let [author, bcp47, uniq] = modelID.split('.'); - - let LMPATH = process.env['LMPATH']; - if (!LMPATH) { - console.error(`${program.name()}: Environment variable LMPATH undefined!`); - program.outputHelp(); - process.exit(EXIT_USAGE); - } - - modelFile = path.join(LMPATH, author, `${bcp47}.${uniq}`, 'build', `${modelID}.model.js`); - } else{ - console.error(`${program.name()}: You forgot to specify a model!`); - program.outputHelp(); - process.exit(EXIT_USAGE); - } + let modelFile = determineModelFile(program); // Ensure we're running in the terminal if (!process.stdin.isTTY) { @@ -204,6 +183,36 @@ function createAsyncWorker() { ///////////////////////////////// Utilities ///////////////////////////////// +/** + * Figure out the path to the model file from the command line. + */ +function determineModelFile(program) { + if (program.modelFile) { + return program.modelFile; + } + + if (program.args[0]) { + // Find the model file under the LMPath. + let modelID = program.args[0]; + let lmPath = process.env['LMPATH']; + let [author, bcp47, uniq] = modelID.split('.'); + + let LMPATH = process.env['LMPATH']; + if (!LMPATH) { + console.error(`${program.name()}: Environment variable LMPATH undefined!`); + program.outputHelp(); + process.exit(EXIT_USAGE); + } + + return path.join(LMPATH, author, `${bcp47}.${uniq}`, 'build', `${modelID}.model.js`); + } + + // A model is not specified in anyway on the command line. Error out! + console.error(`${program.name()}: You forgot to specify a model!`); + program.outputHelp(); + process.exit(EXIT_USAGE); +} + /** * Gets the current context from the current buffer. */ From c7f4dc7187f92403829ea0c43b8a18b78e0de146 Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Mon, 15 Apr 2019 11:48:17 -0600 Subject: [PATCH 25/42] Factor out usageError() function. --- common/test/predictive-text/index.js | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/common/test/predictive-text/index.js b/common/test/predictive-text/index.js index ae63095280..30ea741d79 100644 --- a/common/test/predictive-text/index.js +++ b/common/test/predictive-text/index.js @@ -187,10 +187,13 @@ function createAsyncWorker() { * Figure out the path to the model file from the command line. */ function determineModelFile(program) { + // invoked as: predictive-text -f path/to/model.js if (program.modelFile) { return program.modelFile; } + // invoked as: predictive-text author.bcp47.uniq + // will lookup the model in using LMPATH. if (program.args[0]) { // Find the model file under the LMPath. let modelID = program.args[0]; @@ -199,18 +202,14 @@ function determineModelFile(program) { let LMPATH = process.env['LMPATH']; if (!LMPATH) { - console.error(`${program.name()}: Environment variable LMPATH undefined!`); - program.outputHelp(); - process.exit(EXIT_USAGE); + usageError('Environment variable LMPATH undefined!'); } return path.join(LMPATH, author, `${bcp47}.${uniq}`, 'build', `${modelID}.model.js`); } - // A model is not specified in anyway on the command line. Error out! - console.error(`${program.name()}: You forgot to specify a model!`); - program.outputHelp(); - process.exit(EXIT_USAGE); + // A model is not specified in any way on the command line. Error out! + usageError('You did not specify a model!'); } /** @@ -294,6 +293,15 @@ function unindent(string) { return lines.map(line => line.substr(numLeadingSpaces)).join('\n'); } +/** + * Call to quit the program with an error due to incorrect command line usage. + */ +function usageError(message) { + console.error(`${program.name()}: ${message}\n`); + program.outputHelp(); + process.exit(EXIT_USAGE); +} + /** * True if the keypress looks like something that typically quits the program. */ From 0e1e3c0fd15217a2bb5d37945f2392882ca65781 Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Mon, 15 Apr 2019 12:52:36 -0600 Subject: [PATCH 26/42] Apply predictions to an actual buffer! --- common/test/predictive-text/index.js | 71 +++++++++++++++++----------- 1 file changed, 43 insertions(+), 28 deletions(-) diff --git a/common/test/predictive-text/index.js b/common/test/predictive-text/index.js index 30ea741d79..7525038cd0 100644 --- a/common/test/predictive-text/index.js +++ b/common/test/predictive-text/index.js @@ -70,7 +70,9 @@ function main() { # * Press ${ANSI.BOLD}${ANSI.NORMAL}+${ANSI.BOLD}${ANSI.NORMAL} to quit. `)) - asyncMain(modelFile) + // The command line proper handles asynchronous keypresses, hence the rest + // must also be asynchronous. + asyncRepl(modelFile) .then(_ => process.exit(0)) .catch(err => { console.error(err); @@ -79,7 +81,9 @@ function main() { } -async function asyncMain(modelFile) { +async function asyncRepl(modelFile) { + let buffer = ''; // the text to predict on + // Load the LMLayer and the desired model. let lm = new LMLayer({}, createAsyncWorker()); let config = await lm.loadModel(modelFile); @@ -95,11 +99,44 @@ async function asyncMain(modelFile) { if (wantsQuit(keypress)) { process.stdout.write('\n'); process.exit(0); - } else { - process.stdout.write(char); - let suggestions = Array.from(await lm.predict(insertCharacter(char), getCurrentContext())); - renderSuggestions(suggestions, 0); } + + // TODO: handle tab + // TODO: handle shift-tab + // TODO: handle enter + // TODO: handle backspace + + // insert the keypress + process.stdout.write(char); + let {transform, context} = insertCharacter(char); + let suggestions = Array.from(await lm.predict(transform, context)); + renderSuggestions(suggestions, 0); + } + + // Helpers + + /** + * Inserts a character at the end of the buffer. Returns the transform and + * context of the character insert. + * + * Note: this mutates the buffer! + */ + function insertCharacter(char) { + let oldBuffer = buffer; + buffer += char; + + let transform = { + insert: char, + deleteLeft: 0 + }; + + let context = { + left: oldBuffer, + startOfBuffer: oldBuffer.length === 0 ? true : false, + endOfBuffer: true, + }; + + return { transform, context }; } } @@ -212,28 +249,6 @@ function determineModelFile(program) { usageError('You did not specify a model!'); } -/** - * Gets the current context from the current buffer. - */ -function getCurrentContext() { - // TODO: base this on an actual buffer! - return { - left: '', - startOfBuffer: true, - endOfBuffer: true, - }; -} - -/** - * A transform that inserts a single character. - */ -function insertCharacter(char) { - return { - insert: char, - deleteLeft: 0 - }; -} - /** * Allows you to iterate over each keypress event from stdin. * From 824e8bb1dd91865a727344b826253246b31d42b1 Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Mon, 15 Apr 2019 13:03:53 -0600 Subject: [PATCH 27/42] Handle backspace. --- common/test/predictive-text/index.js | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/common/test/predictive-text/index.js b/common/test/predictive-text/index.js index 7525038cd0..6d88797868 100644 --- a/common/test/predictive-text/index.js +++ b/common/test/predictive-text/index.js @@ -104,13 +104,28 @@ async function asyncRepl(modelFile) { // TODO: handle tab // TODO: handle shift-tab // TODO: handle enter - // TODO: handle backspace - // insert the keypress - process.stdout.write(char); - let {transform, context} = insertCharacter(char); - let suggestions = Array.from(await lm.predict(transform, context)); - renderSuggestions(suggestions, 0); + if (keypress.name === 'backspace') { + if (buffer.length === 0) { + // nothing to do if the buffer is empty + continue; + } + + let oldBuffer = buffer; + // Remove the last character. + // TODO: handle surrogate pairs! + buffer = buffer.substr(0, buffer.length - 1); + + // Redraw the line + process.stdout.write(ANSI.ERASE_IN_LINE() + '\r'); + process.stdout.write(`> ${buffer}`); + } else { + // insert the keypress + process.stdout.write(char); + let {transform, context} = insertCharacter(char); + let suggestions = Array.from(await lm.predict(transform, context)); + renderSuggestions(suggestions, 0); + } } // Helpers From ef7a1e513d5693c5f3a22abbed6dc55b51a0a8e1 Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Mon, 15 Apr 2019 13:20:30 -0600 Subject: [PATCH 28/42] Start to implement insert suggestions. --- common/test/predictive-text/index.js | 37 ++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/common/test/predictive-text/index.js b/common/test/predictive-text/index.js index 6d88797868..2d053a6a07 100644 --- a/common/test/predictive-text/index.js +++ b/common/test/predictive-text/index.js @@ -82,8 +82,6 @@ function main() { async function asyncRepl(modelFile) { - let buffer = ''; // the text to predict on - // Load the LMLayer and the desired model. let lm = new LMLayer({}, createAsyncWorker()); let config = await lm.loadModel(modelFile); @@ -91,6 +89,9 @@ async function asyncRepl(modelFile) { // Setup the REPL // > type some text // [suggestions] [appear] [here] (press tab to accept) + let buffer = ''; // the text to predict on + let suggestions = []; + let selectedSuggestionIndex = 0; // which suggestion is currently suggested // Initial line: process.stdout.write(`> `); @@ -104,8 +105,32 @@ async function asyncRepl(modelFile) { // TODO: handle tab // TODO: handle shift-tab // TODO: handle enter + if (keypress.name === 'return') { + // Accept the currently selected + let acceptedSuggestion = suggestions[selectedSuggestionIndex]; - if (keypress.name === 'backspace') { + if (!acceptedSuggestion) { + // There's no suggestion to insert! + continue; + } + + let oldBuffer = buffer; + let transform = acceptedSuggestion.transform; + + // TODO: Handle deleteLeft from the buffer! + buffer = buffer += transform.insert; + let context = { + left: oldBuffer, + startOfBuffer: oldBuffer.length === 0 ? true : false, + endOfBuffer: true, + } + + // Redraw the line + process.stdout.write(ANSI.ERASE_IN_LINE() + '\r'); + process.stdout.write(`> ${buffer}`); + + // TODO: ask for suggestions again. + } else if (keypress.name === 'backspace') { if (buffer.length === 0) { // nothing to do if the buffer is empty continue; @@ -119,12 +144,14 @@ async function asyncRepl(modelFile) { // Redraw the line process.stdout.write(ANSI.ERASE_IN_LINE() + '\r'); process.stdout.write(`> ${buffer}`); + + // TODO: ask for suggestions again? } else { // insert the keypress process.stdout.write(char); let {transform, context} = insertCharacter(char); - let suggestions = Array.from(await lm.predict(transform, context)); - renderSuggestions(suggestions, 0); + suggestions = Array.from(await lm.predict(transform, context)); + renderSuggestions(suggestions, selectedSuggestionIndex); } } From a613d00116a128904f1711284758a2a6be0061e7 Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Mon, 15 Apr 2019 14:17:29 -0600 Subject: [PATCH 29/42] Handle , +. --- common/test/predictive-text/index.js | 33 +++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/common/test/predictive-text/index.js b/common/test/predictive-text/index.js index 2d053a6a07..f2576c48eb 100644 --- a/common/test/predictive-text/index.js +++ b/common/test/predictive-text/index.js @@ -102,10 +102,32 @@ async function asyncRepl(modelFile) { process.exit(0); } - // TODO: handle tab - // TODO: handle shift-tab - // TODO: handle enter - if (keypress.name === 'return') { + if (keypress.name === 'tab') { + let n = suggestions.length; + if (n === 0) { + // nothing to select. + selectedSuggestionIndex = null; + continue; + } + + if (keypress.shift === false) { + // Select the next suggestion. + selectedSuggestionIndex = (selectedSuggestionIndex + 1) % n; + } else { + // Select the previous suggestion. + + // Note that JavaScript's modulous operator is annoying with negative + // numbers, so use an if statement to handle wrapping. + selectedSuggestionIndex = selectedSuggestionIndex - 1; + // Wrap around to the end. + if (selectedSuggestionIndex < 0) { + selectedSuggestionIndex = n - 1; + } + } + + renderSuggestions(suggestions, selectedSuggestionIndex); + + } else if (keypress.name === 'return') { // Accept the currently selected let acceptedSuggestion = suggestions[selectedSuggestionIndex]; @@ -130,6 +152,7 @@ async function asyncRepl(modelFile) { process.stdout.write(`> ${buffer}`); // TODO: ask for suggestions again. + } else if (keypress.name === 'backspace') { if (buffer.length === 0) { // nothing to do if the buffer is empty @@ -201,7 +224,7 @@ function renderSuggestions(suggestions, selected) { if (index === selected) { return `${ANSI.REVERSE_VIDEO}[${displayAs}]${ANSI.NORMAL_VIDEO}`; } - return `[ ${displayAs} ]` + return `[${displayAs}]` }).join(' '); process.stdout.write(line); } From 2809780b22876168ae3d5fb0d42cfdd2c05a30cc Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Mon, 15 Apr 2019 15:13:35 -0600 Subject: [PATCH 30/42] Explain the LMPATH environment variable. --- common/test/predictive-text/README.md | 46 +++++++++++++++++++-------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/common/test/predictive-text/README.md b/common/test/predictive-text/README.md index a1ef5076ea..93821bce7d 100644 --- a/common/test/predictive-text/README.md +++ b/common/test/predictive-text/README.md @@ -1,4 +1,4 @@ -Predictive Text testing CLI +Predictive text testing CLI --------------------------- Provides a command line interface to try out predictive text models. @@ -10,31 +10,49 @@ Install Install dependencies with `npm`: - npm install + npm install Usage ----- Start it like so: - $ node . + $ node . -f path/to/model.js You will be presented with a prompt. Type any phrase in the model's -language. Press Tab to start choosing a prediction. Press the -left and right arrow keys to go through suggestions. Press -Enter to select the given suggestion. Press Esc. +language. -Press Ctrl+D or Ctrl+C to quit. + * Press Tab to start selecting a suggestion. + * Press Tab again to select the next suggestion. + * Press Shift+Tab to select the previous suggestion. + * Press Enter to accept the selected suggestion. + * Press Ctrl+C or Ctrl+D to quit. > He [ Hello ] [ Hey ] [ He he ] +Defining the `LMPATH` environment variable +------------------------------------------ -ROADMAP -------- +You can also load models by their model ID. For example, - * The command line interface! - * \-f _filename_ to load a model from a file - * `LMPATH` environment variable to load a named model by id: e.g,. - `node . example.crk.wordlist` will load the given model, - provided `LMPATH` is defined. + $ node . example.en.model + +For this to work, you need to define the `LMPATH` environment variable. +The CLI will search the directory specified in `LMPATH` to find the +particular model. + +For example, say you have +[lexical-models](https://github.com/keymanapp/lexical-models) cloned in +your home directory. To be able to access any of the `release` build +lexical models, export the `LMPATH` variable as follows: + +```sh +export LMPATH="${HOME}/lexical-models/release" +``` + +This line usually goes in your shell startup file, e.g., `~/.bashrc`, +`~/.bash_profile`, or `~/.zshrc`, etc. + +Then, any lexical model built in `~/lexical-models/release` can be used +in the command line interface. From b29420f4b65430c7c6529f9b1d24b238401ddc6c Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Mon, 15 Apr 2019 15:26:47 -0600 Subject: [PATCH 31/42] Install executable globally. --- common/test/predictive-text/README.md | 12 +++++++++--- common/test/predictive-text/index.js | 2 ++ common/test/predictive-text/package-lock.json | 2 +- common/test/predictive-text/package.json | 5 +++++ 4 files changed, 17 insertions(+), 4 deletions(-) mode change 100644 => 100755 common/test/predictive-text/index.js diff --git a/common/test/predictive-text/README.md b/common/test/predictive-text/README.md index 93821bce7d..60a7b8ced8 100644 --- a/common/test/predictive-text/README.md +++ b/common/test/predictive-text/README.md @@ -8,16 +8,21 @@ Install **NOTE**: Requires Node >= 10.0 -Install dependencies with `npm`: +Install locally with `npm`: npm install +You can install globally like so: + + npm install -g . + Usage ----- Start it like so: - $ node . -f path/to/model.js + $ node . -f path/to/model.js # local + $ lmlayer-cli . -f path/to/model.js # global You will be presented with a prompt. Type any phrase in the model's language. @@ -36,7 +41,8 @@ Defining the `LMPATH` environment variable You can also load models by their model ID. For example, - $ node . example.en.model + $ node . example.en.model # local + $ lmlayer-cli example.en.model # global For this to work, you need to define the `LMPATH` environment variable. The CLI will search the directory specified in `LMPATH` to find the diff --git a/common/test/predictive-text/index.js b/common/test/predictive-text/index.js old mode 100644 new mode 100755 index f2576c48eb..eba02423b8 --- a/common/test/predictive-text/index.js +++ b/common/test/predictive-text/index.js @@ -1,3 +1,5 @@ +#!/usr/bin/env node + /** * Implements a simple CLI for interactively testing the given predictive text * model. diff --git a/common/test/predictive-text/package-lock.json b/common/test/predictive-text/package-lock.json index 4e29d81798..b6e04296a2 100644 --- a/common/test/predictive-text/package-lock.json +++ b/common/test/predictive-text/package-lock.json @@ -1,6 +1,6 @@ { "name": "lmlayer-cli", - "version": "1.0.0", + "version": "0.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/common/test/predictive-text/package.json b/common/test/predictive-text/package.json index ca4fb51d81..f4a5ed2b22 100644 --- a/common/test/predictive-text/package.json +++ b/common/test/predictive-text/package.json @@ -3,11 +3,16 @@ "version": "0.1.0", "description": "Command line interface to the predictive text engine.", "main": "index.js", + "bin": "./index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "Eddie Antonio Santos ", "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/keymanapp/keyman.git" + }, "dependencies": { "commander": "^2.20.0", "event-iterator": "^1.2.0" From f5971383505d82120bda182b22bb823131f53e07 Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Tue, 16 Apr 2019 09:22:47 -0600 Subject: [PATCH 32/42] Apply the delete left to the buffer as well. --- common/test/predictive-text/index.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/common/test/predictive-text/index.js b/common/test/predictive-text/index.js index eba02423b8..37466c970e 100755 --- a/common/test/predictive-text/index.js +++ b/common/test/predictive-text/index.js @@ -90,7 +90,6 @@ async function asyncRepl(modelFile) { // Setup the REPL // > type some text - // [suggestions] [appear] [here] (press tab to accept) let buffer = ''; // the text to predict on let suggestions = []; let selectedSuggestionIndex = 0; // which suggestion is currently suggested @@ -141,8 +140,8 @@ async function asyncRepl(modelFile) { let oldBuffer = buffer; let transform = acceptedSuggestion.transform; - // TODO: Handle deleteLeft from the buffer! - buffer = buffer += transform.insert; + // apply the transform to the buffer. + buffer = buffer.substr(0, buffer.length - transform.deleteLeft) + transform.insert; let context = { left: oldBuffer, startOfBuffer: oldBuffer.length === 0 ? true : false, @@ -209,6 +208,9 @@ async function asyncRepl(modelFile) { /** * Render the given suggestions on the next line after the cursor. + * Looks like this: + * + * [suggestions] [appear] [here] */ function renderSuggestions(suggestions, selected) { // Wherever we are, save the current cursor position. From 7ad76c43cb3994b6719c0458899d4e861e1dae57 Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Tue, 16 Apr 2019 09:26:18 -0600 Subject: [PATCH 33/42] Factor out applyTransformToActiveBuffer(). --- common/test/predictive-text/index.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/common/test/predictive-text/index.js b/common/test/predictive-text/index.js index 37466c970e..3301e96de9 100755 --- a/common/test/predictive-text/index.js +++ b/common/test/predictive-text/index.js @@ -137,16 +137,7 @@ async function asyncRepl(modelFile) { continue; } - let oldBuffer = buffer; - let transform = acceptedSuggestion.transform; - - // apply the transform to the buffer. - buffer = buffer.substr(0, buffer.length - transform.deleteLeft) + transform.insert; - let context = { - left: oldBuffer, - startOfBuffer: oldBuffer.length === 0 ? true : false, - endOfBuffer: true, - } + applyTransformToActiveBuffer(acceptedSuggestion.transform); // Redraw the line process.stdout.write(ANSI.ERASE_IN_LINE() + '\r'); @@ -204,6 +195,13 @@ async function asyncRepl(modelFile) { return { transform, context }; } + + /** + * Mutates buffer by applying the given transform. + */ + function applyTransformToActiveBuffer(transform) { + buffer = buffer.substr(0, buffer.length - transform.deleteLeft) + transform.insert; + } } /** From d3f4a6eba6cb33fa5d3586833d932a249cc32709 Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Tue, 16 Apr 2019 09:36:49 -0600 Subject: [PATCH 34/42] Factor out deleteLastCodepoint() with surrogate support! --- common/test/predictive-text/index.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/common/test/predictive-text/index.js b/common/test/predictive-text/index.js index 3301e96de9..c1f0189b14 100755 --- a/common/test/predictive-text/index.js +++ b/common/test/predictive-text/index.js @@ -153,8 +153,7 @@ async function asyncRepl(modelFile) { let oldBuffer = buffer; // Remove the last character. - // TODO: handle surrogate pairs! - buffer = buffer.substr(0, buffer.length - 1); + deleteLastCodepoint(); // Redraw the line process.stdout.write(ANSI.ERASE_IN_LINE() + '\r'); @@ -196,6 +195,22 @@ async function asyncRepl(modelFile) { return { transform, context }; } + /** + * Mutates the buffer by removing the last code point. This handles + * surrogate pairs. + */ + function deleteLastCodepoint() { + let lastIndex = buffer.length - 1; + // When the last character is a low surrogate (0xDC00-0xDFFF), this means + // we have a surrogate pair! We must delete two 16-bit code units. + console.log({ buffer }); + if ('\uDC00' <= buffer[lastIndex] && buffer[lastIndex] <= '\uDFFF') { + console.assert('\uD800' <= buffer[lastIndex - 1] && buffer[lastIndex - 1] <= '\uDBFF'); + lastIndex = lastIndex - 1; + } + buffer = buffer.substr(0, lastIndex); + } + /** * Mutates buffer by applying the given transform. */ From 0d4ce258f6f6892d1273452852e222ac0acc4fc7 Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Tue, 16 Apr 2019 09:40:58 -0600 Subject: [PATCH 35/42] Factor out redrawPrompt(). --- common/test/predictive-text/index.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/common/test/predictive-text/index.js b/common/test/predictive-text/index.js index c1f0189b14..653bd1fd85 100755 --- a/common/test/predictive-text/index.js +++ b/common/test/predictive-text/index.js @@ -138,11 +138,7 @@ async function asyncRepl(modelFile) { } applyTransformToActiveBuffer(acceptedSuggestion.transform); - - // Redraw the line - process.stdout.write(ANSI.ERASE_IN_LINE() + '\r'); - process.stdout.write(`> ${buffer}`); - + redrawPrompt(); // TODO: ask for suggestions again. } else if (keypress.name === 'backspace') { @@ -171,6 +167,16 @@ async function asyncRepl(modelFile) { // Helpers + /** + * Draws the prompt, clearing what was on the screen. + */ + function redrawPrompt() { + // Erases the ENTIRE line, returning to the beginning of the line. + process.stdout.write(ANSI.ERASE_IN_LINE() + '\r'); + // Write the buffer! + process.stdout.write(`> ${buffer}`); + } + /** * Inserts a character at the end of the buffer. Returns the transform and * context of the character insert. @@ -203,7 +209,6 @@ async function asyncRepl(modelFile) { let lastIndex = buffer.length - 1; // When the last character is a low surrogate (0xDC00-0xDFFF), this means // we have a surrogate pair! We must delete two 16-bit code units. - console.log({ buffer }); if ('\uDC00' <= buffer[lastIndex] && buffer[lastIndex] <= '\uDFFF') { console.assert('\uD800' <= buffer[lastIndex - 1] && buffer[lastIndex - 1] <= '\uDBFF'); lastIndex = lastIndex - 1; From c3e983aa62ade6f7c9a4d55eabef9cc00686e5ce Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Tue, 16 Apr 2019 09:45:34 -0600 Subject: [PATCH 36/42] Use redrawPrompt() when it's needed. --- common/test/predictive-text/index.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/common/test/predictive-text/index.js b/common/test/predictive-text/index.js index 653bd1fd85..cf395cf580 100755 --- a/common/test/predictive-text/index.js +++ b/common/test/predictive-text/index.js @@ -94,8 +94,8 @@ async function asyncRepl(modelFile) { let suggestions = []; let selectedSuggestionIndex = 0; // which suggestion is currently suggested - // Initial line: - process.stdout.write(`> `); + // Draw the prompt for the first time. + redrawPrompt(); for await (let [char, keypress] of keypressesFromStdin()) { if (wantsQuit(keypress)) { @@ -147,19 +147,14 @@ async function asyncRepl(modelFile) { continue; } - let oldBuffer = buffer; // Remove the last character. deleteLastCodepoint(); - - // Redraw the line - process.stdout.write(ANSI.ERASE_IN_LINE() + '\r'); - process.stdout.write(`> ${buffer}`); + redrawPrompt(); // TODO: ask for suggestions again? } else { - // insert the keypress - process.stdout.write(char); let {transform, context} = insertCharacter(char); + redrawPrompt(); suggestions = Array.from(await lm.predict(transform, context)); renderSuggestions(suggestions, selectedSuggestionIndex); } From 5c7310ef1051483caadbb73ae2618c9fd826cdb6 Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Tue, 16 Apr 2019 09:59:31 -0600 Subject: [PATCH 37/42] Factor out ask for predictions. --- common/test/predictive-text/index.js | 39 +++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/common/test/predictive-text/index.js b/common/test/predictive-text/index.js index cf395cf580..93bc12a57d 100755 --- a/common/test/predictive-text/index.js +++ b/common/test/predictive-text/index.js @@ -139,7 +139,10 @@ async function asyncRepl(modelFile) { applyTransformToActiveBuffer(acceptedSuggestion.transform); redrawPrompt(); - // TODO: ask for suggestions again. + + // Ask for suggestions. + suggestions = await askForPredictions(); + renderSuggestions(suggestions, selectedSuggestionIndex); } else if (keypress.name === 'backspace') { if (buffer.length === 0) { @@ -151,17 +154,47 @@ async function asyncRepl(modelFile) { deleteLastCodepoint(); redrawPrompt(); - // TODO: ask for suggestions again? + // Ask for suggestions again + suggestions = await askForPredictions(); + renderSuggestions(suggestions, selectedSuggestionIndex); + } else { + // Handle a keypress of a letter or symbol. let {transform, context} = insertCharacter(char); redrawPrompt(); - suggestions = Array.from(await lm.predict(transform, context)); + suggestions = await askForPredictions(transform, context); renderSuggestions(suggestions, selectedSuggestionIndex); } } // Helpers + /** + * Asks the LMLayer for predictions with the given transform and context. + * Asynchronously returns the new suggestions. + */ + async function askForPredictions(transform = nullTransform(), context = currentContext()) { + return Array.from(await lm.predict(transform, context)); + } + + /** + * Returns the null transform. + */ + function nullTransform() { + return { insert: '', deleteLeft: 0 }; + } + + /** + * Returns the current state of the buffer. + */ + function currentContext() { + return { + left: buffer, + startOfBuffer: buffer.length === 0, + endOfBuffer: true + }; + } + /** * Draws the prompt, clearing what was on the screen. */ From 11b8a91d317d05dea0f3642efee2547a33316f89 Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Tue, 16 Apr 2019 10:01:19 -0600 Subject: [PATCH 38/42] Fix bug when selectedSuggestedIndex is null. --- common/test/predictive-text/index.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/common/test/predictive-text/index.js b/common/test/predictive-text/index.js index 93bc12a57d..e3bf71a3c6 100755 --- a/common/test/predictive-text/index.js +++ b/common/test/predictive-text/index.js @@ -92,7 +92,7 @@ async function asyncRepl(modelFile) { // > type some text let buffer = ''; // the text to predict on let suggestions = []; - let selectedSuggestionIndex = 0; // which suggestion is currently suggested + let selectedSuggestionIndex = null; // which suggestion is currently suggested // Draw the prompt for the first time. redrawPrompt(); @@ -111,6 +111,13 @@ async function asyncRepl(modelFile) { continue; } + // Nothing selected, but there are suggestions -- then select the first + // one! + if (selectedSuggestionIndex === null) { + selectedSuggestionIndex = 0; + continue; + } + if (keypress.shift === false) { // Select the next suggestion. selectedSuggestionIndex = (selectedSuggestionIndex + 1) % n; @@ -127,7 +134,6 @@ async function asyncRepl(modelFile) { } renderSuggestions(suggestions, selectedSuggestionIndex); - } else if (keypress.name === 'return') { // Accept the currently selected let acceptedSuggestion = suggestions[selectedSuggestionIndex]; From 979746d02c4edb260b1a67bfb20048ab715a59d5 Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Tue, 16 Apr 2019 10:19:34 -0600 Subject: [PATCH 39/42] Move renderSuggestions() into REPL, using REPL variables. --- common/test/predictive-text/index.js | 60 ++++++++++++++-------------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/common/test/predictive-text/index.js b/common/test/predictive-text/index.js index e3bf71a3c6..e906647625 100755 --- a/common/test/predictive-text/index.js +++ b/common/test/predictive-text/index.js @@ -115,6 +115,7 @@ async function asyncRepl(modelFile) { // one! if (selectedSuggestionIndex === null) { selectedSuggestionIndex = 0; + renderSuggestions(); continue; } @@ -134,6 +135,7 @@ async function asyncRepl(modelFile) { } renderSuggestions(suggestions, selectedSuggestionIndex); + } else if (keypress.name === 'return') { // Accept the currently selected let acceptedSuggestion = suggestions[selectedSuggestionIndex]; @@ -148,7 +150,7 @@ async function asyncRepl(modelFile) { // Ask for suggestions. suggestions = await askForPredictions(); - renderSuggestions(suggestions, selectedSuggestionIndex); + renderSuggestions(); } else if (keypress.name === 'backspace') { if (buffer.length === 0) { @@ -162,14 +164,14 @@ async function asyncRepl(modelFile) { // Ask for suggestions again suggestions = await askForPredictions(); - renderSuggestions(suggestions, selectedSuggestionIndex); + renderSuggestions(); } else { // Handle a keypress of a letter or symbol. let {transform, context} = insertCharacter(char); redrawPrompt(); suggestions = await askForPredictions(transform, context); - renderSuggestions(suggestions, selectedSuggestionIndex); + renderSuggestions(); } } @@ -256,36 +258,36 @@ async function asyncRepl(modelFile) { function applyTransformToActiveBuffer(transform) { buffer = buffer.substr(0, buffer.length - transform.deleteLeft) + transform.insert; } -} -/** - * Render the given suggestions on the next line after the cursor. - * Looks like this: - * - * [suggestions] [appear] [here] - */ -function renderSuggestions(suggestions, selected) { - // Wherever we are, save the current cursor position. - process.stdout.write(ANSI.SAVE_CURSOR_POSITION); + /** + * Render the given suggestions on the next line after the cursor. + * Looks like this: + * + * [suggestions] [appear] [here] + */ + function renderSuggestions() { + // Wherever we are, save the current cursor position. + process.stdout.write(ANSI.SAVE_CURSOR_POSITION); - // Jump to next line and erase it to write suggestions. - process.stdout.write(ANSI.CURSOR_NEXT_LINE + ANSI.ERASE_IN_LINE()); + // Jump to next line and erase it to write suggestions. + process.stdout.write(ANSI.CURSOR_NEXT_LINE + ANSI.ERASE_IN_LINE()); - if (!suggestions || suggestions.length === 0) { - process.stdout.write(' no suggestions '); - } else { - // Format the displayed suggestions. - let line = suggestions - .map(({displayAs}, index) => { - if (index === selected) { - return `${ANSI.REVERSE_VIDEO}[${displayAs}]${ANSI.NORMAL_VIDEO}`; - } - return `[${displayAs}]` - }).join(' '); - process.stdout.write(line); + if (!suggestions || suggestions.length === 0) { + process.stdout.write(' no suggestions '); + } else { + // Format the displayed suggestions. + let line = suggestions + .map(({displayAs}, index) => { + if (index === selectedSuggestionIndex) { + return `${ANSI.REVERSE_VIDEO}[${displayAs}]${ANSI.NORMAL_VIDEO}`; + } + return `[${displayAs}]` + }).join(' '); + process.stdout.write(line); + } + + process.stdout.write(ANSI.RESTORE_CURSOR_POSITION); } - - process.stdout.write(ANSI.RESTORE_CURSOR_POSITION); } From b2db7546b8129fde90f18da9d0e95c8d2a0e66b6 Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Tue, 16 Apr 2019 10:22:39 -0600 Subject: [PATCH 40/42] Write 'no suggestions' faintly. --- common/test/predictive-text/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/test/predictive-text/index.js b/common/test/predictive-text/index.js index e906647625..0103f3f428 100755 --- a/common/test/predictive-text/index.js +++ b/common/test/predictive-text/index.js @@ -32,6 +32,7 @@ const ANSI = { SAVE_CURSOR_POSITION: CSI + 's', // Remembers the current cursor position. RESTORE_CURSOR_POSITION: CSI + 'u', // Moves the cursor to the previously stored position. BOLD: CSI + '1m', // Set bold text. + FAINT: CSI + '2m', // Set "faint" text. REVERSE_VIDEO: CSI + '7m', // Invert the text colour (swap background and foreground colours). NORMAL_VIDEO: CSI + '27m', // Uninvert the text colour (swap background and foreground colours). NORMAL: CSI + 'm', // Set all graphics renditions attributes off. @@ -273,7 +274,7 @@ async function asyncRepl(modelFile) { process.stdout.write(ANSI.CURSOR_NEXT_LINE + ANSI.ERASE_IN_LINE()); if (!suggestions || suggestions.length === 0) { - process.stdout.write(' no suggestions '); + process.stdout.write(`${ANSI.FAINT}no suggestions${ANSI.NORMAL}`); } else { // Format the displayed suggestions. let line = suggestions From 9ac0c865e78d1ccd6e8dd66baa88ba0f13ab5122 Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Tue, 16 Apr 2019 11:12:11 -0600 Subject: [PATCH 41/42] Implement the prediction protocol correctly. --- common/test/predictive-text/index.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/common/test/predictive-text/index.js b/common/test/predictive-text/index.js index 0103f3f428..bd617d2219 100755 --- a/common/test/predictive-text/index.js +++ b/common/test/predictive-text/index.js @@ -92,6 +92,10 @@ async function asyncRepl(modelFile) { // Setup the REPL // > type some text let buffer = ''; // the text to predict on + // To support the prediction API, the previous buffer must be kept + // to apply suggested transforms correctly: + // See: https://github.com/keymanapp/keyman/blob/99caf52b38fddedfb0969b20c0e6b27be243c348/common/predictive-text/docs/worker-communication-protocol.md#message-predict + let previousBuffer = null; let suggestions = []; let selectedSuggestionIndex = null; // which suggestion is currently suggested @@ -221,7 +225,7 @@ async function asyncRepl(modelFile) { * Note: this mutates the buffer! */ function insertCharacter(char) { - let oldBuffer = buffer; + previousBuffer = buffer; buffer += char; let transform = { @@ -230,8 +234,8 @@ async function asyncRepl(modelFile) { }; let context = { - left: oldBuffer, - startOfBuffer: oldBuffer.length === 0 ? true : false, + left: previousBuffer, + startOfBuffer: previousBuffer.length === 0 ? true : false, endOfBuffer: true, }; @@ -257,7 +261,10 @@ async function asyncRepl(modelFile) { * Mutates buffer by applying the given transform. */ function applyTransformToActiveBuffer(transform) { - buffer = buffer.substr(0, buffer.length - transform.deleteLeft) + transform.insert; + let tmpBuffer = buffer; + buffer = previousBuffer + .substr(0, previousBuffer.length - transform.deleteLeft) + transform.insert; + previousBuffer = tmpBuffer; } /** From 4d719af761d893ca4d8cfd956b11c334bde55b86 Mon Sep 17 00:00:00 2001 From: Eddie Antonio Santos Date: Tue, 16 Apr 2019 11:17:26 -0600 Subject: [PATCH 42/42] Spruce up some things in README.md. --- common/test/predictive-text/README.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/common/test/predictive-text/README.md b/common/test/predictive-text/README.md index 60a7b8ced8..a6c3af5d08 100644 --- a/common/test/predictive-text/README.md +++ b/common/test/predictive-text/README.md @@ -1,5 +1,5 @@ Predictive text testing CLI ---------------------------- +=========================== Provides a command line interface to try out predictive text models. @@ -21,7 +21,7 @@ Usage Start it like so: - $ node . -f path/to/model.js # local + $ ./index.js -f path/to/model.js # local $ lmlayer-cli . -f path/to/model.js # global You will be presented with a prompt. Type any phrase in the model's @@ -33,16 +33,18 @@ language. * Press Enter to accept the selected suggestion. * Press Ctrl+C or Ctrl+D to quit. - > He - [ Hello ] [ Hey ] [ He he ] +``` +> He +[Hello] [Hey] [He he] +``` Defining the `LMPATH` environment variable ------------------------------------------ You can also load models by their model ID. For example, - $ node . example.en.model # local - $ lmlayer-cli example.en.model # global + $ ./index.js example.en.model # local + $ lmlayer-cli example.en.model # global For this to work, you need to define the `LMPATH` environment variable. The CLI will search the directory specified in `LMPATH` to find the