@topcli/prompts 0.0.1

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.
package/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2023 Pierre Demailly
2
+
3
+ Permission to use, copy, modify, and/or distribute this software for any purpose
4
+ with or without fee is hereby granted, provided that the above copyright notice
5
+ and this permission notice appear in all copies.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
8
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
9
+ FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
10
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
11
+ OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
12
+ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
13
+ THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # prompts
2
+
3
+ ![version](https://img.shields.io/badge/dynamic/json.svg?style=for-the-badge&url=https://raw.githubusercontent.com/TopCli/prompts/main/package.json&query=$.version&label=Version)
4
+ [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg?style=for-the-badge)](https://github.com/TopCli/prompts/commit-activity)
5
+ [![isc](https://img.shields.io/badge/License-ISC-blue.svg?style=for-the-badge)](https://github.com/TopCli/prompts/blob/main/LICENSE)
6
+ ![build](https://img.shields.io/github/actions/workflow/status/TopCli/prompts/node.js.yml?style=for-the-badge)
7
+
8
+ Node.js user input library for command-line interfaces.
9
+
10
+ ## Requirements
11
+ - [Node.js](https://nodejs.org/en/) v14 or higher
12
+
13
+ ## Getting Started
14
+
15
+ > **Note** This package is ESM only.
16
+
17
+ This package is available in the Node Package Repository and can be easily installed with [npm](https://docs.npmjs.com/getting-started/what-is-npm) or [yarn](https://yarnpkg.com).
18
+
19
+ ```bash
20
+ $ npm i @topcli/prompts
21
+ # or
22
+ $ yarn add @topcli/prompts
23
+ ```
24
+
25
+ ## Usage exemple
26
+
27
+ ```js
28
+ import { prompt, select, confirm } from '@topcli/prompts'
29
+
30
+ const name = await prompt('What\'s your name ?')
31
+ const gender = await select('What\'s your gender ?', {
32
+ choices: [
33
+ {
34
+ value: 'M',
35
+ label: 'Male'
36
+ },
37
+ {
38
+ value: 'F',
39
+ label: 'Female'
40
+ },
41
+ ]
42
+ })
43
+ const isAdult = await confirm('Are you over 18 ?', { initial: true })
44
+ ```
45
+
46
+ ## API
47
+
48
+ ### `prompt(message: string): Promise<string>`
49
+
50
+ Simple prompt, similar to `rl.question()` with an improved UI.
51
+
52
+ ### `select(message: string, options: { choices: (Choice | string)[], maxVisible?: number }): Promise<string>`
53
+
54
+ Scrollable select depending `maxVisible` (default `8`).
55
+
56
+ ### `confirm(message: string, options?: { initial: boolean }): Promise<string>`
57
+
58
+ Boolean prompt.
59
+
60
+ ## Interfaces
61
+
62
+ ```ts
63
+ export interface Choice {
64
+ value: any,
65
+ label: string,
66
+ description?: string,
67
+ }
68
+ ```
package/index.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ export interface Choice {
2
+ value: any,
3
+ label: string,
4
+ description?: string,
5
+ }
6
+ export function prompt(message: string): Promise<string>;
7
+ export function select(message: string, options: { choices: (Choice | string)[], maxVisble?: number }): Promise<string>;
8
+ export function confirm(message: string, options?: { initial: boolean }): Promise<string>;
package/index.js ADDED
@@ -0,0 +1,167 @@
1
+ import { createInterface } from 'node:readline/promises'
2
+
3
+ import ansi from 'ansi-styles'
4
+
5
+ const kQuestionMark = `${ansi.blue.open}?${ansi.blue.close}`
6
+ const kTick = `${ansi.green.open}✔${ansi.green.close}`
7
+ const kCross = `${ansi.red.open}✖${ansi.red.close}`
8
+ const kPointer = `${ansi.gray.open}›${ansi.gray.close}`
9
+ const kActive = `${ansi.reset.open}${kPointer} `
10
+ const kInactive = `${ansi.gray.open} `
11
+ const kPrevious = '⭡'
12
+ const kNext = '⭣'
13
+ const kShowCursor = '\x1B[?25h'
14
+ const kHideCursor = '\x1B[?25l'
15
+
16
+ function clearLastLine () {
17
+ process.stdout.moveCursor(0, -1)
18
+ process.stdout.clearLine()
19
+ }
20
+
21
+ export async function prompt (message) {
22
+ if (typeof message !== 'string') {
23
+ throw new TypeError('message must be a string')
24
+ }
25
+
26
+ const { stdin: input, stdout: output } = process
27
+ const rl = createInterface({ input, output })
28
+
29
+ const answer = await rl.question(`${ansi.bold.open} ${kPointer} ${message} ${ansi.bold.close}`)
30
+
31
+ clearLastLine()
32
+ console.log(`${ansi.bold.open}${answer ? kTick : kCross} ${message} ${kPointer} ${ansi.yellow.open}${answer}${ansi.yellow.close}${ansi.bold.close}`)
33
+
34
+ rl.close()
35
+
36
+ return answer
37
+ }
38
+
39
+ export async function select (message, options) {
40
+ if (typeof message !== 'string') {
41
+ throw new TypeError('message must be a string')
42
+ }
43
+
44
+ if (!options) {
45
+ throw new TypeError('Missing required options')
46
+ }
47
+
48
+ const { choices } = options
49
+
50
+ if (!choices?.length) {
51
+ throw new TypeError('Missing required param: choices')
52
+ }
53
+
54
+ const longestChoice = Math.max(...choices.map(choice => {
55
+ if (typeof choice === 'string') {
56
+ return choice.length
57
+ }
58
+
59
+ const kRequiredChoiceProperties = ['label', 'value']
60
+
61
+ for (const prop of kRequiredChoiceProperties) {
62
+ if (!choice[prop]) {
63
+ throw new TypeError(`Missing ${prop} for choice ${JSON.stringify(choice)}`)
64
+ }
65
+ }
66
+
67
+ return choice.label.length
68
+ }))
69
+
70
+ process.stdout.write(kHideCursor)
71
+ const { stdin: input, stdout: output } = process
72
+ const rl = createInterface({ input, output })
73
+
74
+ let activeIndex = 0
75
+
76
+ console.log(`${ansi.bold.open}${kQuestionMark} ${message}${ansi.bold.close}`)
77
+
78
+ let lastRender = null
79
+ const render = (initialRender = false, { reset } = {}) => {
80
+ const getVisibleChoices = (currentIndex, total, maxVisible) => {
81
+ maxVisible = maxVisible || total
82
+
83
+ let startIndex = Math.min(total - maxVisible, currentIndex - Math.floor(maxVisible / 2))
84
+ if (startIndex < 0) {
85
+ startIndex = 0
86
+ }
87
+
88
+ const endIndex = Math.min(startIndex + maxVisible, total)
89
+
90
+ return { startIndex, endIndex }
91
+ }
92
+
93
+ const { startIndex, endIndex } = getVisibleChoices(activeIndex, choices.length, options.maxVisible || 8)
94
+
95
+ if (!initialRender) {
96
+ const linesToClear = lastRender.endIndex - lastRender.startIndex
97
+ process.stdout.clearLine(1)
98
+ process.stdout.moveCursor(0, -linesToClear)
99
+ process.stdout.clearLine(1)
100
+ }
101
+
102
+ if (reset) {
103
+ clearLastLine()
104
+ clearLastLine()
105
+ return
106
+ }
107
+
108
+ lastRender = { startIndex, endIndex }
109
+
110
+ for (let i = startIndex; i < endIndex; i++) {
111
+ const choice = typeof choices[i] === 'string' ? { value: choices[i], label: choices[i] } : choices[i]
112
+ const prefix = `${startIndex > 0 && i === startIndex ? kPrevious : endIndex < choices.length && i === endIndex - 1 ? kNext : ' '}${i === activeIndex ? kActive : kInactive}`
113
+ const str = `${prefix}${choice.label.padEnd(longestChoice < 10 ? longestChoice : 0)}${choice.description ? ` - ${choice.description}` : ''}${ansi.reset.open}`
114
+ console.log(str)
115
+ }
116
+ }
117
+
118
+ render(true)
119
+
120
+ return new Promise((resolve) => {
121
+ const onKeypress = (value, key) => {
122
+ if (key.name === 'up') {
123
+ activeIndex = activeIndex === 0 ? choices.length - 1 : activeIndex - 1
124
+ render()
125
+ } else if (key.name === 'down') {
126
+ activeIndex = activeIndex === choices.length - 1 ? 0 : activeIndex + 1
127
+ render()
128
+ } else if (key.name === 'return') {
129
+ process.stdin.off('keypress', onKeypress)
130
+ render(false, { reset: true })
131
+
132
+ console.log(`${ansi.bold.open}${kTick} ${message} ${kPointer} ${ansi.yellow.open}${choices[activeIndex].value}${ansi.reset.open}`)
133
+
134
+ process.stderr.write(kShowCursor)
135
+ rl.close()
136
+
137
+ resolve(choices[activeIndex].value)
138
+ }
139
+ }
140
+
141
+ process.stdin.on('keypress', onKeypress)
142
+ })
143
+ }
144
+
145
+ const kDefaultConfirmOptions = { initial: false }
146
+
147
+ export async function confirm (message, options = kDefaultConfirmOptions) {
148
+ if (typeof message !== 'string') {
149
+ throw new TypeError('message must be a string')
150
+ }
151
+
152
+ const { initial } = { kDefaultConfirmOptions, ...options }
153
+
154
+ const { stdin: input, stdout: output } = process
155
+ const rl = createInterface({ input, output })
156
+
157
+ const tip = initial ? `${ansi.bold.open}Yes${ansi.bold.close}/no` : `yes/${ansi.bold.open}No${ansi.bold.close}`
158
+ const answer = await rl.question(`${kQuestionMark} ${message} ${ansi.grey.open}(${tip})${ansi.grey.close} ${kPointer} `)
159
+ const result = (!answer && initial) || ['y', 'yes'].includes(answer.toLocaleLowerCase())
160
+
161
+ clearLastLine()
162
+ console.log(`${result ? kTick : kCross} ${message}`)
163
+
164
+ rl.close()
165
+
166
+ return result
167
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@topcli/prompts",
3
+ "version": "0.0.1",
4
+ "description": "Node.js user input library for command-line interfaces.",
5
+ "scripts": {
6
+ "test": "node --test ./test/**.test.js",
7
+ "lint": "standard --fix | snazzy"
8
+ },
9
+ "main": "./index.js",
10
+ "keywords": [
11
+ "node.js",
12
+ "cli",
13
+ "prompt"
14
+ ],
15
+ "files": [
16
+ "index.js",
17
+ "index.d.ts"
18
+ ],
19
+ "author": "PierreDemailly <pierredemailly.pro@gmail.com>",
20
+ "license": "ISC",
21
+ "type": "module",
22
+ "devDependencies": {
23
+ "snazzy": "^9.0.0",
24
+ "standard": "^17.0.0"
25
+ },
26
+ "dependencies": {
27
+ "ansi-styles": "^6.2.1"
28
+ },
29
+ "engines": {
30
+ "node": ">=14"
31
+ },
32
+ "bugs": {
33
+ "url": "https://github.com/TopCli/prompts/issues"
34
+ },
35
+ "homepage": "https://github.com/TopCli/prompts#readme",
36
+ "publishConfig": {
37
+ "access": "public"
38
+ }
39
+ }