@hitools/tui 0.1.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.
package/element.js ADDED
@@ -0,0 +1,46 @@
1
+ export default class Element {
2
+
3
+ static types = {}
4
+
5
+ static registerType(type, component) {
6
+ this.types[type] = component
7
+
8
+ if(component.alias) {
9
+ this.types[component.alias] = component
10
+ }
11
+
12
+ }
13
+
14
+ constructor (params = {}, {theme} = {}) {
15
+
16
+ if (typeof params === 'string') {
17
+ params = { type: 'message', text: params }
18
+ }
19
+
20
+ const { type } = params
21
+ if (!type) throw new Error('Element type required')
22
+
23
+ const Component = Element.types[type]
24
+ if (!Component) throw new Error(`Unknown type "${type}"`)
25
+
26
+ const data = {}
27
+
28
+ for (const key in Component.schema) {
29
+ const rule = Component.schema[key]
30
+ const value = params[key] ?? rule.default
31
+
32
+ if (rule.required && (value === undefined || value === null)) {
33
+ throw new Error(`"${key}" required for type "${type}"`)
34
+ }
35
+
36
+ data[key] = value
37
+ }
38
+
39
+ if (theme) data.theme = theme
40
+ if (params.template) data.template = params.template
41
+ if (params.before) data.before = params.before
42
+ if (params.after) data.after = params.after
43
+
44
+ return new Component(data)
45
+ }
46
+ }
@@ -0,0 +1,56 @@
1
+ import Prompt from '../prompt.js'
2
+
3
+ export default class Confirm extends Prompt {
4
+
5
+ type = 'confirm'
6
+
7
+ constructor(data) {
8
+ super(data)
9
+ this.value = this.defaultValue ?? true
10
+ }
11
+
12
+ static schema = {
13
+ label: { required: true },
14
+ defaultValue: { default: false },
15
+ name: { required: false },
16
+ }
17
+
18
+ actions() {
19
+ return {
20
+ 'y': () => this.value = true,
21
+ 'n': () => this.value = false,
22
+ 'left': () => this.value = true,
23
+ 'right': () => this.value = false,
24
+ 'return': () => this.finished = true,
25
+ 'space': () => this.finished = true
26
+ }
27
+ }
28
+
29
+ build () {
30
+ this.clearLastRender()
31
+
32
+ const options = [
33
+ { title: 'Yes', value: true },
34
+ { title: 'No', value: false }
35
+ ]
36
+
37
+ let output
38
+
39
+ if (this.template) {
40
+ const renderedOptions = options.map(opt => {
41
+ const state = opt.value === this.value ? this.useTemplate(opt, this.template.option.selected) : this.useTemplate(opt, this.template.option.unselected)
42
+ return state
43
+ }).join(' /')
44
+
45
+ output = this.useTemplate({label: this.label, options: renderedOptions}, this.template.current)
46
+
47
+ } else {
48
+ const yes = this.value ? '> Yes' : ' Yes'
49
+ const no = !this.value ? '> No' : ' No'
50
+ output = `\n${this.label}\n${yes} / ${no}\n`
51
+ }
52
+
53
+ this.lines = output.split('\n').length
54
+ return output
55
+ }
56
+ }
@@ -0,0 +1,22 @@
1
+ import Prompt from '../prompt.js'
2
+
3
+ export default class ErrorMessage extends Prompt {
4
+
5
+ type = 'error'
6
+
7
+ static schema = {
8
+ label: { required: true }
9
+ }
10
+
11
+ async build() {
12
+ this.finished = true
13
+
14
+ if(this.template) {
15
+ return this.useTemplate(this, this.template)
16
+ } else {
17
+ return `Error: ${this.label}\n`
18
+ }
19
+
20
+ }
21
+
22
+ }
@@ -0,0 +1,19 @@
1
+ import Message from './message.js'
2
+ import Select from './select.js'
3
+ import MultiSelect from './multiselect.js'
4
+ import Confirm from './confirm.js'
5
+ import ErrorMessage from './error.js'
6
+ import Loader from './loader.js'
7
+ import Question from './question.js'
8
+ import Task from './task.js'
9
+
10
+ export default {
11
+ message: Message,
12
+ question: Question,
13
+ confirm: Confirm,
14
+ select: Select,
15
+ multiselect: MultiSelect,
16
+ error: ErrorMessage,
17
+ loader: Loader,
18
+ task: Task,
19
+ }
@@ -0,0 +1,47 @@
1
+ import Prompt from '../prompt.js'
2
+
3
+ export default class Loader extends Prompt {
4
+
5
+ type = 'loader'
6
+
7
+ static schema = {
8
+ label: { required: true },
9
+ time: { required: true }
10
+ }
11
+
12
+ async build() {
13
+
14
+ return await new Promise( (resolve, reject) => {
15
+
16
+ Prompt.out.write( this.useTemplate(this, this.template) )
17
+ const spinnerFrames = Prompt.theme.spinner
18
+
19
+ let i = 0
20
+
21
+ const loader = setInterval(() => {
22
+ Prompt.out.clearLine(0)
23
+ Prompt.out.cursorTo(0)
24
+ Prompt.out.write(spinnerFrames[i])
25
+ i = (i + 1) % spinnerFrames.length
26
+ }, 80)
27
+
28
+
29
+ setTimeout( () => {
30
+ clearInterval(loader)
31
+ Prompt.out.clearLine(0)
32
+ Prompt.out.cursorTo(0)
33
+ this.finished = true
34
+ if(this.template) {
35
+ // resolve(this.useTemplate('✔ Done\n', this.template))
36
+ resolve('✔ Done')
37
+ // resolve(this.useTemplate('✔ Done\n'))
38
+ } else {
39
+ return `Done\n`
40
+ }
41
+ }, this.time)
42
+
43
+ })
44
+
45
+ }
46
+
47
+ }
@@ -0,0 +1,23 @@
1
+ import Prompt from '../prompt.js'
2
+
3
+ export default class Message extends Prompt {
4
+
5
+ type = 'message'
6
+
7
+ static schema = {
8
+ label: { required: true },
9
+ text: { required: false }
10
+ }
11
+
12
+ build() {
13
+ this.finished = true
14
+
15
+ if(this.template) {
16
+ return this.useTemplate(this, this.template)
17
+ } else {
18
+ return `${this.label}\n`
19
+ }
20
+
21
+ }
22
+
23
+ }
@@ -0,0 +1,62 @@
1
+ import Prompt from '../prompt.js'
2
+
3
+ export default class MultiSelect extends Prompt {
4
+
5
+ type = 'multiselect'
6
+
7
+ constructor(data) {
8
+ super(data)
9
+ this.value = this.defaultValue
10
+ this.index = 0
11
+
12
+ this.options = this.prepareOptions(this.options)
13
+ }
14
+
15
+ static schema = {
16
+ label: { required: true },
17
+ options: { required: true },
18
+ defaultValue: { default: null },
19
+ name: { required: false },
20
+ }
21
+
22
+ actions() {
23
+ return {
24
+ up: () => this.index = Math.max(0, this.index-1),
25
+ down: () => this.index = Math.min(this.options.length-1, this.index+1),
26
+ space: () => this.options[this.index].selected = this.options[this.index].selected ? false : true,
27
+ return: () => {
28
+ this.finished = true
29
+ this.value = this.options.filter(i => i.selected)
30
+ }
31
+ }
32
+ }
33
+
34
+ build () {
35
+
36
+ this.clearLastRender()
37
+
38
+ let output
39
+
40
+ if (this.template) {
41
+
42
+ const renderedOptions = this.options.map( (opt, i) => {
43
+ let state = i === this.index ? this.template.option.startCurrent : this.template.option.start
44
+ state += opt.selected ? this.useTemplate(opt, this.template.option.selected) : this.useTemplate(opt, this.template.option.unselected)
45
+ return state
46
+ }).join('')
47
+
48
+ output = this.useTemplate({label: this.label, options: renderedOptions}, this.finished ? this.template.finished : this.template.current)
49
+
50
+ } else {
51
+ output = `\n${this.label}`
52
+ options.forEach( (opt, i) => {
53
+ output += i === this.index ? `\n > [ ] ${opt.title}` : `\n [ ] ${opt.title}`
54
+ })
55
+ output += '\n'
56
+ }
57
+
58
+ this.lines = output.split('\n').length
59
+ return output
60
+ }
61
+
62
+ }
@@ -0,0 +1,52 @@
1
+ import Prompt from '../prompt.js'
2
+
3
+ export default class Question extends Prompt {
4
+
5
+ type = 'question'
6
+ static alias = 'ask'
7
+
8
+ constructor (data) {
9
+ super(data)
10
+ this.value = this.defaultValue
11
+ }
12
+
13
+ static schema = {
14
+ label: { required: true },
15
+ placeholder: { required: true },
16
+ defaultValue: { default: '' },
17
+ name: { required: false },
18
+ }
19
+
20
+ actions () {
21
+ return {
22
+ return: () => {
23
+ if(!this.value) this.value = this.placeholder
24
+ this.finished = true
25
+ }
26
+ }
27
+ }
28
+
29
+ build () {
30
+
31
+ this.clearLastRender()
32
+
33
+ let output
34
+
35
+ if (this.template) {
36
+ output = this.useTemplate(this, this.template)
37
+ } else {
38
+
39
+ output = `\n${this.label}`
40
+ if(this.value) {
41
+ output += ` ${this.value}`
42
+ } else {
43
+ output += ` ${this.placeholder}`
44
+ }
45
+
46
+ }
47
+
48
+ this.lines = output.split('\n').length
49
+ return output
50
+ }
51
+
52
+ }
@@ -0,0 +1,60 @@
1
+ import Prompt from '../prompt.js'
2
+
3
+ export default class Select extends Prompt {
4
+
5
+ type = 'select'
6
+
7
+ constructor(data) {
8
+ super(data)
9
+ this.value = this.defaultValue
10
+ this.index = 0
11
+
12
+ this.options = this.prepareOptions(this.options)
13
+ }
14
+
15
+ static schema = {
16
+ label: { required: true },
17
+ options: { required: true },
18
+ defaultValue: { default: null },
19
+ name: { required: false },
20
+ }
21
+
22
+ actions() {
23
+ return {
24
+ up: () => this.index = Math.max(0, this.index-1),
25
+ down: () => this.index = Math.min(this.options.length-1, this.index+1),
26
+ return: () => {
27
+ this.finished = true
28
+ this.value = this.options[this.index]
29
+ }
30
+ }
31
+ }
32
+
33
+ build () {
34
+
35
+ this.clearLastRender()
36
+
37
+ let output
38
+
39
+ if (this.template) {
40
+
41
+ const renderedOptions = this.options.map( (opt, i) => {
42
+ const state = i === this.index ? this.useTemplate(opt, this.template.option.selected) : this.useTemplate(opt, this.template.option.unselected)
43
+ return state
44
+ }).join('')
45
+
46
+ output = this.useTemplate({label: this.label, options: renderedOptions}, this.finished ? this.template.finished : this.template.current)
47
+
48
+ } else {
49
+ output = `\n${this.label}`
50
+ options.forEach( (opt, i) => {
51
+ output += i === this.index ? `\n > ${opt.title}` : `\n ${opt.title}`
52
+ })
53
+ output += '\n'
54
+ }
55
+
56
+ this.lines = output.split('\n').length
57
+ return output
58
+ }
59
+
60
+ }
@@ -0,0 +1,25 @@
1
+ import Prompt from '../prompt.js'
2
+
3
+ export default class Task extends Prompt {
4
+
5
+ type = 'task'
6
+
7
+ static schema = {
8
+ run: { required: true },
9
+ params: { required: false },
10
+ name: { required: false },
11
+ }
12
+
13
+ async build () {
14
+
15
+ Prompt.out.write( this.useTemplate(this, this.template) )
16
+
17
+ let {value, finished} = await this.run(this, this.params)
18
+ if(value) this.value = value
19
+ if(finished) this.finished = finished
20
+
21
+ return value ?? null
22
+
23
+ }
24
+
25
+ }
@@ -0,0 +1 @@
1
+ // in progress
package/index.js ADDED
@@ -0,0 +1,85 @@
1
+ import Components from './elements/index.js'
2
+ import Element from './element.js'
3
+ import Prompt from './prompt.js'
4
+
5
+ import Theme from './theme.js'
6
+ import Template from '/Users/hodunay/Packages/drafts/templates/index.js'
7
+ import Validator from '/Users/hodunay/Packages/drafts/cli-manager/validate/validate.js'
8
+
9
+ // Registering Components
10
+ Object.keys(Components).forEach(c => Element.registerType(c.toLowerCase(), Components[c]))
11
+
12
+ export default class TUI {
13
+
14
+ constructor(config) {
15
+
16
+ config.theme = true
17
+ config.validator = true
18
+
19
+ Prompt.use('in', process.stdin)
20
+ Prompt.use('out', process.stdout)
21
+
22
+ this.useTypes()
23
+ this.setup(config)
24
+ }
25
+
26
+ useTypes () {
27
+ Object.keys(Element.types).map( type => {
28
+ this[type] = (args) => {
29
+
30
+ let params = {}
31
+ if (typeof args === 'string') {
32
+ params.type = 'message'
33
+ params.label = args
34
+ }
35
+
36
+ // todo: refactor
37
+ let exists = Element.types[type]
38
+ if (exists) {
39
+ params = {
40
+ type: exists.type ?? exists.alias,
41
+ ...args
42
+ }
43
+ }
44
+
45
+ this.flow.push( new Element(params) )
46
+ }
47
+ })
48
+ }
49
+
50
+ setup (config) {
51
+
52
+ if(Array.isArray(config)) {
53
+ this.flow = config.map(el => new Element(el))
54
+ }
55
+
56
+ if(config.theme) {
57
+ Prompt.use('theme', Theme)
58
+ Prompt.use('templateEngine', new Template())
59
+ // Prompt.use('theme', config.theme)
60
+ }
61
+
62
+ if(config.flow) {
63
+ this.flow = config.flow ? config.flow.map(el => new Element(el)) : []
64
+ }
65
+
66
+ if(config.validator) {
67
+ Prompt.use('validate', Validator)
68
+ // Prompt.use('validator', config.validator)
69
+ }
70
+
71
+ if(config.elements) {
72
+ config.elements.map( el => {
73
+ Element.registerType(el.name, el.component)
74
+ })
75
+ }
76
+ }
77
+
78
+ async run(context = {}) {
79
+ for (const el of this.flow) {
80
+ const result = await el.render()
81
+ if (el.name && result) context[el.name] = result.value
82
+ }
83
+ return context
84
+ }
85
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@hitools/tui",
3
+ "version": "0.1.0",
4
+ "description": "Terminal UI with basic components set like Question, Select, Multi-select, Message. ",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "test": "jest"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/hodunay/tui.git"
13
+ },
14
+ "keywords": [
15
+ "tui",
16
+ "cli",
17
+ "nodejs",
18
+ "javascript"
19
+ ],
20
+ "author": "Igor Hodunay",
21
+ "license": "MIT",
22
+ "bugs": {
23
+ "url": "https://github.com/hodunay/tui/issues"
24
+ },
25
+ "homepage": "https://github.com/hodunay/tui#readme",
26
+ "directories": {
27
+ "example": "examples"
28
+ }
29
+ }
package/prompt.js ADDED
@@ -0,0 +1,152 @@
1
+ import readline from 'readline'
2
+
3
+ function hideCursor () {
4
+ process.stdout.write('\x1B[?25l')
5
+ }
6
+
7
+ function showCursor () {
8
+ process.stdout.write('\x1B[?25l')
9
+ }
10
+
11
+ process.on('exit', showCursor)
12
+ process.on('SIGINT', showCursor)
13
+
14
+ export default class Prompt {
15
+
16
+ constructor (data) {
17
+ Object.assign(this, data)
18
+ this.finished = false
19
+ this.lines = 0
20
+ }
21
+
22
+ static use (type, data) {
23
+ this[type] = data
24
+ }
25
+
26
+ setup () {
27
+ readline.emitKeypressEvents(Prompt.in)
28
+ if (Prompt.in.isTTY) Prompt.in.setRawMode(true)
29
+
30
+ if(Prompt.templateEngine)
31
+ this.template = Prompt.theme ? Prompt.theme.template[this.type] : false
32
+ }
33
+
34
+ clear () {
35
+ Prompt.out.cursorTo(0, 0)
36
+ Prompt.out.clearScreenDown()
37
+ }
38
+
39
+ teardown () {
40
+ if (Prompt.in.isTTY) Prompt.in.setRawMode(false)
41
+ }
42
+
43
+ prepareOptions (options, assign = false) {
44
+ if(Array.isArray(options)) {
45
+ options = options.map( (item, i) => {
46
+ let opt = { title: item, value: i }
47
+ // if(assign) opt = Object.assign(opt, assign)
48
+ return opt
49
+ })
50
+ return options
51
+ }
52
+
53
+ else if (typeof options === 'object') {
54
+ return options
55
+ }
56
+ }
57
+
58
+ clearLastRender () {
59
+ if (!this.lines) return
60
+
61
+ for (let i = 0; i < this.lines -1; i++) {
62
+ Prompt.out.clearLine(0)
63
+ Prompt.out.moveCursor(0, -1)
64
+ }
65
+
66
+ Prompt.out.cursorTo(0)
67
+ }
68
+
69
+ async render () {
70
+
71
+ if (this.before) this.before(this)
72
+
73
+ this.setup()
74
+
75
+ if(!this.actions?.()) {
76
+ let response = await this.build()
77
+ Prompt.out.write( response )
78
+ return response
79
+ }
80
+
81
+ return await new Promise((resolve, reject) => {
82
+
83
+ const cleanup = () => {
84
+ Prompt.in.off('keypress', handler)
85
+ this.teardown()
86
+ Prompt.out.write('\x1B[?25h') // show cursor
87
+ }
88
+
89
+ const handler = (chunk, key) => {
90
+
91
+ if (key?.ctrl && key.name === 'c') {
92
+ cleanup()
93
+ return reject(new Error('Interrupted'))
94
+ }
95
+
96
+ const actions = this.actions?.()
97
+
98
+ if (typeof actions?.[key?.name] === 'function') {
99
+ hideCursor()
100
+ actions[key.name]()
101
+ Prompt.out.write( this.build() )
102
+ }
103
+
104
+ else if (!this.finished && key?.name && chunk) {
105
+
106
+ if (key?.name === 'backspace') {
107
+ this.value = this.value.slice(0, -1)
108
+ } else {
109
+ this.value = this.value + chunk.toString()
110
+ }
111
+
112
+ Prompt.out.clearLine()
113
+ Prompt.out.cursorTo(0)
114
+ Prompt.out.write(this.build())
115
+
116
+ // validate realtime
117
+ }
118
+
119
+ if (this.finished) {
120
+ cleanup()
121
+ if (this.after) this.after(this)
122
+ resolve(this)
123
+ }
124
+
125
+ // to clear full screen
126
+ // this.clear()
127
+ // Prompt.out.write(this.build())
128
+
129
+
130
+ }
131
+
132
+ if(Prompt.validator) {
133
+ // console.log(Prompt.validator)
134
+ // const validation = Prompt.validator.check( state.value.length ? state.value : state.defaultValue, validator, { live: false } )
135
+ // state.error = !validation.valid
136
+ // state.errorMessage = validation.error
137
+ }
138
+
139
+ Prompt.in.on('keypress', handler)
140
+ let response = this.build()
141
+ Prompt.out.write( response )
142
+ showCursor()
143
+
144
+ })
145
+
146
+ }
147
+
148
+ useTemplate (data, template = false) {
149
+ return Prompt.templateEngine ? Prompt.templateEngine(data, template) : false
150
+ }
151
+
152
+ }