spyne-cli 0.6.7 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +90 -38
  3. package/index.js +123 -21
  4. package/package.json +24 -6
  5. package/src/cli/args.js +197 -0
  6. package/src/cli/render.js +184 -0
  7. package/src/core/create-app.js +232 -0
  8. package/src/core/generate-module.js +146 -0
  9. package/src/registry.js +174 -0
  10. package/src/spyne-file-prompt.js +77 -30
  11. package/src/templates/generate-prompt-input-fields.js +1 -1
  12. package/src/ui.js +5 -1
  13. package/lib/ansi.js +0 -116
  14. package/lib/combos.js +0 -75
  15. package/lib/completer.js +0 -52
  16. package/lib/interpolate.js +0 -266
  17. package/lib/keypress.js +0 -243
  18. package/lib/placeholder.js +0 -63
  19. package/lib/prompt.js +0 -485
  20. package/lib/prompts/autocomplete.js +0 -113
  21. package/lib/prompts/basicauth.js +0 -41
  22. package/lib/prompts/confirm.js +0 -13
  23. package/lib/prompts/editable.js +0 -136
  24. package/lib/prompts/form.js +0 -196
  25. package/lib/prompts/index.js +0 -28
  26. package/lib/prompts/input.js +0 -55
  27. package/lib/prompts/invisible.js +0 -11
  28. package/lib/prompts/list.js +0 -36
  29. package/lib/prompts/multiselect.js +0 -11
  30. package/lib/prompts/numeral.js +0 -1
  31. package/lib/prompts/password.js +0 -18
  32. package/lib/prompts/quiz.js +0 -37
  33. package/lib/prompts/scale.js +0 -237
  34. package/lib/prompts/select.js +0 -139
  35. package/lib/prompts/snippet.js +0 -185
  36. package/lib/prompts/sort.js +0 -37
  37. package/lib/prompts/survey.js +0 -163
  38. package/lib/prompts/text.js +0 -1
  39. package/lib/prompts/toggle.js +0 -109
  40. package/lib/render.js +0 -33
  41. package/lib/roles.js +0 -46
  42. package/lib/state.js +0 -69
  43. package/lib/styles.js +0 -144
  44. package/lib/symbols.js +0 -66
  45. package/lib/theme.js +0 -11
  46. package/lib/timer.js +0 -38
  47. package/lib/types/array.js +0 -658
  48. package/lib/types/auth.js +0 -29
  49. package/lib/types/boolean.js +0 -88
  50. package/lib/types/index.js +0 -7
  51. package/lib/types/number.js +0 -86
  52. package/lib/types/string.js +0 -185
  53. package/lib/utils.js +0 -268
  54. package/mocha.conf.cjs +0 -16
  55. package/src/app/channels/.gitkeep +0 -0
  56. package/src/app/channels/channel-custom.js +0 -1
  57. package/src/app/components/.gitkeep +0 -0
  58. package/src/app/traits/.gitkeep +0 -0
  59. package/src/hello-world-app-source/README.md +0 -15
  60. package/src/hello-world-app-source/apache-htaccess +0 -20
  61. package/src/hello-world-app-source/karma.conf.js +0 -75
  62. package/src/hello-world-app-source/package.json +0 -50
  63. package/src/hello-world-app-source/src/app/app-view.js +0 -38
  64. package/src/hello-world-app-source/src/app/channels/.gitkeep +0 -0
  65. package/src/hello-world-app-source/src/app/components/.gitkeep +0 -0
  66. package/src/hello-world-app-source/src/app/traits/.gitkeep +0 -0
  67. package/src/hello-world-app-source/src/index.js +0 -16
  68. package/src/hello-world-app-source/src/index.tmpl.html +0 -12
  69. package/src/hello-world-app-source/src/scss/01-vendors/normalize.css +0 -349
  70. package/src/hello-world-app-source/src/scss/02-variables/mixins.scss +0 -0
  71. package/src/hello-world-app-source/src/scss/03-components/.gitkeep +0 -0
  72. package/src/hello-world-app-source/src/scss/main.scss +0 -15
  73. package/src/hello-world-app-source/src/static/data/.gitkeep +0 -0
  74. package/src/hello-world-app-source/src/static/fonts/.gitkeep +0 -0
  75. package/src/hello-world-app-source/src/static/imgs/.gitkeep +0 -0
  76. package/src/hello-world-app-source/src/tests/unit-tests/index.test.js +0 -10
  77. package/src/hello-world-app-source/webpack.config.js +0 -178
  78. package/src/spyne-app-create.js +0 -209
  79. package/src/spyne-app-creator.js +0 -119
  80. package/src/spyne-starter-app-create.js +0 -147
  81. package/tests/create-spyne-app.test.js +0 -63
  82. package/tests/generate-file-prompt.test.js +0 -45
  83. package/tests/generate-file-string.test.js +0 -66
  84. package/tests/generate-prompt-input-fields-methods.test.js +0 -177
  85. package/tests/generate-prompt-input-fields.test.js +0 -179
  86. package/tests/generate-prompt-input-object.test.js +0 -39
  87. package/tests/generate-prompt-output.test.js +0 -58
  88. package/tests/index.test.js +0 -11
  89. package/tests/mocks/answers.js +0 -33
  90. package/tests/mocks/answers.json +0 -33
  91. package/tests/mocks/enquirer-data.js +0 -411
  92. package/tests/mocks/enquirer-data.json +0 -409
  93. package/tests/utils/file-utils.test.js +0 -84
@@ -0,0 +1,174 @@
1
+ // Single source of truth for every spyne-cli command.
2
+ //
3
+ // Three consumers read this registry: the interactive picker, the argv parser,
4
+ // and (in a later release) the SpyneJS MCP server. Adding a command here makes
5
+ // it available to all three — no other file needs to change.
6
+
7
+ import {createApp, DEFAULT_TEMPLATE, TEMPLATES} from './core/create-app.js';
8
+ import {generateModule} from './core/generate-module.js';
9
+
10
+ const templateValues = Object.keys(TEMPLATES);
11
+
12
+ const moduleArgs = (extra = {}) => ({
13
+ fileName: {
14
+ type: 'string',
15
+ positional: 0,
16
+ required: true,
17
+ description: 'File name for the generated module (.js is appended if omitted).',
18
+ },
19
+ className: {
20
+ type: 'string',
21
+ description: 'Class name. Defaults to the PascalCase form of the file name.',
22
+ },
23
+ fileDirectory: {
24
+ type: 'string',
25
+ alias: 'd',
26
+ description: 'Directory to write into. Defaults to the conventional path for this type.',
27
+ },
28
+ ...extra,
29
+ });
30
+
31
+ /**
32
+ * A generation target: interactive via the picker, deterministic via flags,
33
+ * enumerable programmatically.
34
+ *
35
+ * @typedef {Object} Command
36
+ * @property {string} name command name as typed
37
+ * @property {string} summary one line, shown in help and the picker
38
+ * @property {'app'|'module'} kind app creates a directory; module writes into a project
39
+ * @property {Object} args arg name -> schema
40
+ * @property {Function} handler async (args) => structured result
41
+ */
42
+ export const commands = [
43
+ {
44
+ name: 'create-app',
45
+ summary: 'Create a new SpyneJS application',
46
+ pickerLabel: 'App',
47
+ kind: 'app',
48
+ args: {
49
+ appName: {
50
+ type: 'string',
51
+ positional: 0,
52
+ required: true,
53
+ description: 'Directory name for the new application.',
54
+ },
55
+ template: {
56
+ type: 'string',
57
+ alias: 't',
58
+ enum: templateValues,
59
+ default: DEFAULT_TEMPLATE,
60
+ description: 'Template to scaffold from.',
61
+ },
62
+ install: {
63
+ type: 'boolean',
64
+ default: true,
65
+ negatable: true,
66
+ description: 'Install dependencies after scaffolding.',
67
+ negatedDescription: 'Skip dependency installation.',
68
+ },
69
+ git: {
70
+ type: 'boolean',
71
+ default: true,
72
+ negatable: true,
73
+ description: 'Initialise a fresh git repository.',
74
+ negatedDescription: 'Skip git initialisation.',
75
+ },
76
+ },
77
+ handler: createApp,
78
+ },
79
+ {
80
+ name: 'create-viewstream',
81
+ summary: 'Generate a ViewStream class',
82
+ pickerLabel: 'ViewStream',
83
+ kind: 'module',
84
+ fileType: 'ViewStream',
85
+ args: moduleArgs(),
86
+ handler: (args) => generateModule({...args, fileType: 'ViewStream'}),
87
+ },
88
+ {
89
+ name: 'create-domelement',
90
+ summary: 'Generate a DomElement class',
91
+ pickerLabel: 'DomElement',
92
+ kind: 'module',
93
+ fileType: 'DomElement',
94
+ args: moduleArgs(),
95
+ handler: (args) => generateModule({...args, fileType: 'DomElement'}),
96
+ },
97
+ {
98
+ name: 'create-channel',
99
+ summary: 'Generate a Channel class and register it',
100
+ pickerLabel: 'Channel',
101
+ kind: 'module',
102
+ fileType: 'Channel',
103
+ args: moduleArgs({
104
+ channelName: {
105
+ type: 'string',
106
+ description: 'Channel name. Defaults to the CONSTANT_CASE form of the class name.',
107
+ },
108
+ replayLastPayload: {
109
+ type: 'boolean',
110
+ default: false,
111
+ description: 'Send the cached payload to new subscribers.',
112
+ },
113
+ }),
114
+ handler: (args) => generateModule({...args, fileType: 'Channel'}),
115
+ },
116
+ {
117
+ name: 'create-trait',
118
+ summary: 'Generate a SpyneTrait class',
119
+ pickerLabel: 'SpyneTrait',
120
+ kind: 'module',
121
+ fileType: 'SpyneTrait',
122
+ args: moduleArgs({
123
+ methodPrefix: {
124
+ type: 'string',
125
+ description: 'String prepended to all trait methods. Derived from the class name by default.',
126
+ },
127
+ }),
128
+ handler: (args) => generateModule({...args, fileType: 'SpyneTrait'}),
129
+ },
130
+ ];
131
+
132
+ // `new` shipped in 0.6.x and stays working; create-app is canonical.
133
+ export const aliases = {
134
+ new: {command: 'create-app', deprecated: true, since: '0.7.0'},
135
+ };
136
+
137
+ /**
138
+ * Flags that existed in 0.6.x and no longer do. Declared rather than deleted so
139
+ * the CLI can say what happened instead of reporting an unknown option.
140
+ */
141
+ export const removedFlags = {
142
+ spa: {
143
+ removedIn: '0.7.0',
144
+ message: '"--spa" has been removed. Pick a template with --template starter|shell.',
145
+ },
146
+ };
147
+
148
+ export const commandNames = commands.map((c) => c.name);
149
+
150
+ export const getCommand = (name) => {
151
+ if (!name) return undefined;
152
+ const alias = aliases[name];
153
+ const target = alias ? alias.command : name;
154
+ return commands.find((c) => c.name === target);
155
+ };
156
+
157
+ export const getAlias = (name) => aliases[name];
158
+
159
+ export const moduleCommands = commands.filter((c) => c.kind === 'module');
160
+
161
+ export const appCommands = commands.filter((c) => c.kind === 'app');
162
+
163
+ /**
164
+ * Registry as plain data — no handlers, no functions. This is the shape the
165
+ * MCP server will enumerate to expose each generation target as a capability.
166
+ */
167
+ export const describeCommands = () => commands.map(
168
+ ({name, summary, kind, args}) => ({
169
+ name,
170
+ summary,
171
+ kind,
172
+ args: Object.fromEntries(
173
+ Object.entries(args).map(([argName, schema]) => [argName, {...schema}])),
174
+ }));
@@ -1,14 +1,25 @@
1
1
  import enquirer from 'enquirer';
2
2
  const {prompt, Select} = enquirer;
3
+ import c from 'ansi-colors';
3
4
 
4
5
  import {Data} from './spyne-template-prompts.js';
5
6
  const {promptInputHash} = Data;
6
7
  import GeneratePromptInputObject from './templates/generate-prompt-input-object.js';
7
- import GenerateFileString from './templates/generate-file-string.js';
8
- import {onSaveSpyneFileToDir} from './utils/file-utils.js';
9
- import {generatePromptOutput} from './templates/generate-prompt-output.js';
10
- import {addChannelToIndexJS} from './utils/add-channel-to-index-file.js';
11
- import {insertChannelStrings} from './utils/insert-channel-strings-to-index-file.js';
8
+ import {appCommands, moduleCommands} from './registry.js';
9
+ import {generateModule} from './core/generate-module.js';
10
+ import {createApp, detectSpyneProject} from './core/create-app.js';
11
+ import {
12
+ appNamePrompt,
13
+ createProgressReporter,
14
+ renderAppResult,
15
+ renderError,
16
+ renderModuleResult,
17
+ selectSizing,
18
+ templatePrompt,
19
+ } from './cli/render.js';
20
+
21
+ // Picker labels come from the registry, so command messaging has one home.
22
+ const APP_LABELS = new Set(appCommands.map((cmd) => cmd.pickerLabel));
12
23
 
13
24
  export default class SpyneFilePrompt {
14
25
 
@@ -32,48 +43,84 @@ export default class SpyneFilePrompt {
32
43
  return props.map(mapInputTypesToPrompObj);
33
44
  }
34
45
 
46
+ /**
47
+ * The picker is a renderer over the registry: App first, then every module
48
+ * command in registry order.
49
+ */
35
50
  getSelectPromptObj() {
36
51
  const promptGen = new GeneratePromptInputObject('selectFileType');
37
- return promptGen.getPrompObject();
52
+ const promptObj = promptGen.getPrompObject();
38
53
 
54
+ promptObj.choices = [
55
+ ...appCommands.map((cmd) => ({
56
+ name: cmd.pickerLabel,
57
+ message: c.greenBright(cmd.pickerLabel),
58
+ hint: c.dim(`— ${cmd.summary}`),
59
+ })),
60
+ ...moduleCommands.map((cmd) => c.greenBright(cmd.pickerLabel)),
61
+ ];
62
+
63
+ Object.assign(promptObj, selectSizing(promptObj.choices.length));
64
+
65
+ return promptObj;
39
66
  }
40
67
 
41
- saveFileAndSendOutput(answers) {
42
- const {fileType, fileName, fileDirectory, className} = answers;
43
- const {fileString} = new GenerateFileString(fileType, answers);
44
- const savedProps = onSaveSpyneFileToDir(fileString, fileName, fileDirectory);
68
+ async saveFileAndSendOutput(answers) {
69
+ const result = await generateModule(answers);
45
70
 
46
- // default is no channel to be registered, yet
47
- let channelHasRegistered = false;
71
+ if (!result.ok && result.error.code === 'NOT_IN_SPYNE_PROJECT') {
72
+ console.log(renderError(result.error));
73
+ process.exitCode = 1;
74
+ return result;
75
+ }
48
76
 
49
- if (fileType === 'Channel'){
50
- //const savedChannelToIndexProps = addChannelToIndexJS(className, fileName);
51
- const savedChannelToIndexProps = insertChannelStrings(className, fileName);
77
+ console.log(renderModuleResult(result));
78
+ if (!result.ok) process.exitCode = 1;
79
+ return result;
80
+ }
52
81
 
53
- // check if channel has been registered
54
- channelHasRegistered = savedChannelToIndexProps.fileHasSaved;
82
+ async runAppPrompt() {
83
+ const {isSpyneProject, root} = detectSpyneProject();
84
+
85
+ if (isSpyneProject) {
86
+ const {proceed} = await prompt({
87
+ type: 'confirm',
88
+ name: 'proceed',
89
+ initial: false,
90
+ message: c.yellow(
91
+ `This creates a new app inside an existing SpyneJS project (${root}) — continue?`),
92
+ });
93
+ if (!proceed) return undefined;
55
94
  }
56
95
 
57
- const msgOutput = generatePromptOutput(answers, savedProps, fileString, channelHasRegistered);
96
+ // Template first: the general choice before the specific one.
97
+ const answers = await prompt([templatePrompt(), appNamePrompt()]);
58
98
 
99
+ const result = await createApp({
100
+ ...answers,
101
+ onProgress: createProgressReporter(),
102
+ });
59
103
 
60
- console.log(msgOutput);
104
+ console.log(result.ok
105
+ ? renderAppResult(result)
106
+ : renderError(result.error));
107
+
108
+ if (!result.ok) process.exitCode = 1;
109
+ return result;
61
110
  }
62
111
 
63
112
  async startPrompt() {
64
- const selectPromptObj = this.getSelectPromptObj();
65
- //console.log('select prompt obj is ',selectPromptObj);
66
- const selectPrompt = new Select(selectPromptObj);
67
- const onSelectComplete = async (fileType) => {
68
- const filePromptArr = SpyneFilePrompt.getFilePrompt(fileType);
69
- //console.log("promp arr is ",filePromptArr);
70
- const filePrompt = await prompt(filePromptArr);
71
- const answersObj = Object.assign({}, filePrompt, {fileType});
72
- this.saveFileAndSendOutput(answersObj);
73
- };
113
+ const selectPrompt = new Select(this.getSelectPromptObj());
114
+ const selection = await selectPrompt.run();
74
115
 
75
- selectPrompt.run().then(onSelectComplete);
116
+ if (APP_LABELS.has(selection)) {
117
+ return this.runAppPrompt();
118
+ }
76
119
 
120
+ const filePromptArr = SpyneFilePrompt.getFilePrompt(selection);
121
+ const filePrompt = await prompt(filePromptArr);
122
+ return this.saveFileAndSendOutput(
123
+ Object.assign({}, filePrompt, {fileType: selection}));
77
124
  }
78
125
 
79
126
  };
@@ -174,7 +174,7 @@ const generateMesage = () => {
174
174
  const defaultMsgFn = messageColor(`Enter the ${_fileType} file name`);
175
175
 
176
176
  const inputTypeHash = {
177
- selectFileType: messageColor(`Select file type`),
177
+ selectFileType: messageColor(`What would you like to create?`),
178
178
  fileName: defaultMsgFn,
179
179
  className: messageColor(`Enter the ${_fileType} class name`),
180
180
  fileDirectory: messageColor(`Enter where you would like to save the ${_fileType} file`),
package/src/ui.js CHANGED
@@ -1,11 +1,15 @@
1
1
  import figlet from 'figlet';
2
2
  import chalk from 'chalk';
3
+ import { readFileSync } from 'fs';
4
+
5
+ const { version } = JSON.parse(
6
+ readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
3
7
 
4
8
  export class SpyneCliUI {
5
9
 
6
10
  constructor() {}
7
11
  static title(){
8
- const figletTxt = figlet.textSync('spyne-cli 6.7', {
12
+ const figletTxt = figlet.textSync(`spyne-cli ${version}`, {
9
13
  horizontalLayout: 'universal smushing'
10
14
  })
11
15
  const chalkOutput = chalk.blue(figletTxt);
package/lib/ansi.js DELETED
@@ -1,116 +0,0 @@
1
- 'use strict';
2
-
3
- const isTerm = process.env.TERM_PROGRAM === 'Apple_Terminal';
4
- const colors = require('ansi-colors');
5
- const utils = require('./utils');
6
- const ansi = module.exports = exports;
7
- const ESC = '\u001b[';
8
- const BEL = '\u0007';
9
- let hidden = false;
10
-
11
- const code = ansi.code = {
12
- bell: BEL,
13
- beep: BEL,
14
- beginning: `${ESC}G`,
15
- down: `${ESC}J`,
16
- esc: ESC,
17
- getPosition: `${ESC}6n`,
18
- hide: `${ESC}?25l`,
19
- line: `${ESC}2K`,
20
- lineEnd: `${ESC}K`,
21
- lineStart: `${ESC}1K`,
22
- restorePosition: ESC + (isTerm ? '8' : 'u'),
23
- savePosition: ESC + (isTerm ? '7' : 's'),
24
- screen: `${ESC}2J`,
25
- show: `${ESC}?25h`,
26
- up: `${ESC}1J`
27
- };
28
-
29
- const cursor = ansi.cursor = {
30
- get hidden() {
31
- return hidden;
32
- },
33
-
34
- hide() {
35
- hidden = true;
36
- return code.hide;
37
- },
38
- show() {
39
- hidden = false;
40
- return code.show;
41
- },
42
-
43
- forward: (count = 1) => `${ESC}${count}C`,
44
- backward: (count = 1) => `${ESC}${count}D`,
45
- nextLine: (count = 1) => `${ESC}E`.repeat(count),
46
- prevLine: (count = 1) => `${ESC}F`.repeat(count),
47
-
48
- up: (count = 1) => count ? `${ESC}${count}A` : '',
49
- down: (count = 1) => count ? `${ESC}${count}B` : '',
50
- right: (count = 1) => count ? `${ESC}${count}C` : '',
51
- left: (count = 1) => count ? `${ESC}${count}D` : '',
52
-
53
- to(x, y) {
54
- return y ? `${ESC}${y + 1};${x + 1}H` : `${ESC}${x + 1}G`;
55
- },
56
-
57
- move(x = 0, y = 0) {
58
- let res = '';
59
- res += (x < 0) ? cursor.left(-x) : (x > 0) ? cursor.right(x) : '';
60
- res += (y < 0) ? cursor.up(-y) : (y > 0) ? cursor.down(y) : '';
61
- return res;
62
- },
63
-
64
- restore(state = {}) {
65
- let { after, cursor, initial, input, prompt, size, value } = state;
66
- initial = utils.isPrimitive(initial) ? String(initial) : '';
67
- input = utils.isPrimitive(input) ? String(input) : '';
68
- value = utils.isPrimitive(value) ? String(value) : '';
69
-
70
- if (size) {
71
- let codes = ansi.cursor.up(size) + ansi.cursor.to(prompt.length);
72
- let diff = input.length - cursor;
73
- if (diff > 0) {
74
- codes += ansi.cursor.left(diff);
75
- }
76
- return codes;
77
- }
78
-
79
- if (value || after) {
80
- let pos = (!input && !!initial) ? -initial.length : -input.length + cursor;
81
- if (after) pos -= after.length;
82
- if (input === '' && initial && !prompt.includes(initial)) {
83
- pos += initial.length;
84
- }
85
- return ansi.cursor.move(pos);
86
- }
87
- }
88
- };
89
-
90
- const erase = ansi.erase = {
91
- screen: code.screen,
92
- up: code.up,
93
- down: code.down,
94
- line: code.line,
95
- lineEnd: code.lineEnd,
96
- lineStart: code.lineStart,
97
- lines(n) {
98
- let str = '';
99
- for (let i = 0; i < n; i++) {
100
- str += ansi.erase.line + (i < n - 1 ? ansi.cursor.up(1) : '');
101
- }
102
- if (n) str += ansi.code.beginning;
103
- return str;
104
- }
105
- };
106
-
107
- ansi.clear = (input = '', columns = process.stdout.columns) => {
108
- if (!columns) return erase.line + cursor.to(0);
109
- let width = str => [...colors.unstyle(str)].length;
110
- let lines = input.split(/\r?\n/);
111
- let rows = 0;
112
- for (let line of lines) {
113
- rows += 1 + Math.floor(Math.max(width(line) - 1, 0) / columns);
114
- }
115
- return (erase.line + cursor.prevLine()).repeat(rows - 1) + erase.line + cursor.to(0);
116
- };
package/lib/combos.js DELETED
@@ -1,75 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * Actions are mappings from keypress event names to method names
5
- * in the prompts.
6
- */
7
-
8
- exports.ctrl = {
9
- a: 'first',
10
- b: 'backward',
11
- c: 'cancel',
12
- d: 'deleteForward',
13
- e: 'last',
14
- f: 'forward',
15
- g: 'reset',
16
- i: 'tab',
17
- k: 'cutForward',
18
- l: 'reset',
19
- n: 'newItem',
20
- m: 'cancel',
21
- j: 'submit',
22
- p: 'search',
23
- r: 'remove',
24
- s: 'save',
25
- u: 'undo',
26
- w: 'cutLeft',
27
- x: 'toggleCursor',
28
- v: 'paste'
29
- };
30
-
31
- exports.shift = {
32
- up: 'shiftUp',
33
- down: 'shiftDown',
34
- left: 'shiftLeft',
35
- right: 'shiftRight',
36
- tab: 'prev'
37
- };
38
-
39
- exports.fn = {
40
- up: 'pageUp',
41
- down: 'pageDown',
42
- left: 'pageLeft',
43
- right: 'pageRight',
44
- delete: 'deleteForward'
45
- };
46
-
47
- // <alt> on Windows
48
- exports.option = {
49
- b: 'backward',
50
- f: 'forward',
51
- d: 'cutRight',
52
- left: 'cutLeft',
53
- up: 'altUp',
54
- down: 'altDown'
55
- };
56
-
57
- exports.keys = {
58
- pageup: 'pageUp', // <fn>+<up> (mac), <Page Up> (windows)
59
- pagedown: 'pageDown', // <fn>+<down> (mac), <Page Down> (windows)
60
- home: 'home', // <fn>+<left> (mac), <home> (windows)
61
- end: 'end', // <fn>+<right> (mac), <end> (windows)
62
- cancel: 'cancel',
63
- delete: 'deleteForward',
64
- backspace: 'delete',
65
- down: 'down',
66
- enter: 'submit',
67
- escape: 'cancel',
68
- left: 'left',
69
- space: 'space',
70
- number: 'number',
71
- return: 'submit',
72
- right: 'right',
73
- tab: 'next',
74
- up: 'up'
75
- };
package/lib/completer.js DELETED
@@ -1,52 +0,0 @@
1
- 'use strict';
2
-
3
- const unique = arr => arr.filter((v, i) => arr.lastIndexOf(v) === i);
4
- const compact = arr => unique(arr).filter(Boolean);
5
-
6
- module.exports = (action, data = {}, value = '') => {
7
- let { past = [], present = '' } = data;
8
- let rest, prev;
9
-
10
- switch (action) {
11
- case 'prev':
12
- case 'undo':
13
- rest = past.slice(0, past.length - 1);
14
- prev = past[past.length - 1] || '';
15
- return {
16
- past: compact([value, ...rest]),
17
- present: prev
18
- };
19
-
20
- case 'next':
21
- case 'redo':
22
- rest = past.slice(1);
23
- prev = past[0] || '';
24
- return {
25
- past: compact([...rest, value]),
26
- present: prev
27
- };
28
-
29
- case 'save':
30
- return {
31
- past: compact([...past, value]),
32
- present: ''
33
- };
34
-
35
- case 'remove':
36
- prev = compact(past.filter(v => v !== value));
37
- present = '';
38
-
39
- if (prev.length) {
40
- present = prev.pop();
41
- }
42
-
43
- return {
44
- past: prev,
45
- present
46
- };
47
-
48
- default: {
49
- throw new Error(`Invalid action: "${action}"`);
50
- }
51
- }
52
- };