tty-prompt 0.21.0 → 0.22.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +37 -1
- data/README.md +372 -215
- data/lib/tty-prompt.rb +1 -2
- data/lib/tty/prompt.rb +145 -142
- data/lib/tty/prompt/answers_collector.rb +2 -2
- data/lib/tty/prompt/block_paginator.rb +1 -1
- data/lib/tty/prompt/choice.rb +1 -1
- data/lib/tty/prompt/choices.rb +28 -11
- data/lib/tty/prompt/confirm_question.rb +23 -12
- data/lib/tty/prompt/const.rb +17 -0
- data/lib/tty/prompt/converter_dsl.rb +6 -7
- data/lib/tty/prompt/converter_registry.rb +31 -26
- data/lib/tty/prompt/converters.rb +139 -32
- data/lib/tty/prompt/enum_list.rb +27 -19
- data/lib/tty/prompt/errors.rb +31 -0
- data/lib/tty/prompt/evaluator.rb +1 -1
- data/lib/tty/prompt/expander.rb +20 -12
- data/lib/tty/prompt/keypress.rb +3 -3
- data/lib/tty/prompt/list.rb +57 -25
- data/lib/tty/prompt/mask_question.rb +2 -2
- data/lib/tty/prompt/multi_list.rb +71 -27
- data/lib/tty/prompt/multiline.rb +6 -5
- data/lib/tty/prompt/paginator.rb +1 -1
- data/lib/tty/prompt/question.rb +56 -31
- data/lib/tty/prompt/question/checks.rb +18 -0
- data/lib/tty/prompt/selected_choices.rb +76 -0
- data/lib/tty/prompt/slider.rb +67 -8
- data/lib/tty/prompt/statement.rb +3 -3
- data/lib/tty/prompt/suggestion.rb +5 -5
- data/lib/tty/prompt/symbols.rb +58 -58
- data/lib/tty/prompt/test.rb +36 -0
- data/lib/tty/prompt/utils.rb +1 -3
- data/lib/tty/prompt/version.rb +1 -1
- metadata +12 -24
- data/lib/tty/prompt/messages.rb +0 -49
- data/lib/tty/test_prompt.rb +0 -20
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative
|
3
|
+
require_relative "question"
|
4
4
|
|
5
5
|
module TTY
|
6
6
|
class Prompt
|
@@ -75,7 +75,7 @@ module TTY
|
|
75
75
|
def read_input(question)
|
76
76
|
@done_masked = false
|
77
77
|
@failure = false
|
78
|
-
@input =
|
78
|
+
@input = ""
|
79
79
|
@prompt.print(question)
|
80
80
|
until @done_masked
|
81
81
|
@prompt.read_keypress
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative
|
3
|
+
require_relative "list"
|
4
|
+
require_relative "selected_choices"
|
4
5
|
|
5
6
|
module TTY
|
6
7
|
class Prompt
|
@@ -9,8 +10,6 @@ module TTY
|
|
9
10
|
#
|
10
11
|
# @api private
|
11
12
|
class MultiList < List
|
12
|
-
HELP = '(Use %s arrow%s keys, press Space to select and Enter to finish%s)'
|
13
|
-
|
14
13
|
# Create instance of TTY::Prompt::MultiList menu.
|
15
14
|
#
|
16
15
|
# @param [Prompt] :prompt
|
@@ -19,7 +18,7 @@ module TTY
|
|
19
18
|
# @api public
|
20
19
|
def initialize(prompt, **options)
|
21
20
|
super
|
22
|
-
@selected =
|
21
|
+
@selected = SelectedChoices.new
|
23
22
|
@help = options[:help]
|
24
23
|
@echo = options.fetch(:echo, true)
|
25
24
|
@min = options[:min]
|
@@ -44,11 +43,11 @@ module TTY
|
|
44
43
|
#
|
45
44
|
# @api private
|
46
45
|
def keyenter(*)
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
46
|
+
valid = true
|
47
|
+
valid = @min <= @selected.size if @min
|
48
|
+
valid = @selected.size <= @max if @max
|
49
|
+
|
50
|
+
super if valid
|
52
51
|
end
|
53
52
|
alias keyreturn keyenter
|
54
53
|
|
@@ -58,13 +57,33 @@ module TTY
|
|
58
57
|
def keyspace(*)
|
59
58
|
active_choice = choices[@active - 1]
|
60
59
|
if @selected.include?(active_choice)
|
61
|
-
@selected.
|
60
|
+
@selected.delete_at(@active - 1)
|
62
61
|
else
|
63
62
|
return if @max && @selected.size >= @max
|
64
|
-
@selected
|
63
|
+
@selected.insert(@active - 1, active_choice)
|
65
64
|
end
|
66
65
|
end
|
67
66
|
|
67
|
+
# Selects all choices when Ctrl+A is pressed
|
68
|
+
#
|
69
|
+
# @api private
|
70
|
+
def keyctrl_a(*)
|
71
|
+
return if @max && @max < choices.size
|
72
|
+
@selected = SelectedChoices.new(choices.enabled, choices.enabled_indexes)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Revert currently selected choices when Ctrl+I is pressed
|
76
|
+
#
|
77
|
+
# @api private
|
78
|
+
def keyctrl_r(*)
|
79
|
+
return if @max && @max < choices.size
|
80
|
+
indexes = choices.each_with_index.reduce([]) do |acc, (choice, idx)|
|
81
|
+
acc << idx if !choice.disabled? && !@selected.include?(choice)
|
82
|
+
acc
|
83
|
+
end
|
84
|
+
@selected = SelectedChoices.new(choices.enabled - @selected.to_a, indexes)
|
85
|
+
end
|
86
|
+
|
68
87
|
private
|
69
88
|
|
70
89
|
# Setup default options and active selection
|
@@ -73,7 +92,9 @@ module TTY
|
|
73
92
|
def setup_defaults
|
74
93
|
validate_defaults
|
75
94
|
# At this stage, @choices matches all the visible choices.
|
76
|
-
|
95
|
+
default_indexes = @default.map { |d| d - 1 }
|
96
|
+
@selected = SelectedChoices.new(@choices.values_at(*default_indexes),
|
97
|
+
default_indexes)
|
77
98
|
|
78
99
|
if !@default.empty?
|
79
100
|
@active = @default.last
|
@@ -88,7 +109,7 @@ module TTY
|
|
88
109
|
#
|
89
110
|
# @api private
|
90
111
|
def selected_names
|
91
|
-
@selected.map(&:name).join(
|
112
|
+
@selected.map(&:name).join(", ")
|
92
113
|
end
|
93
114
|
|
94
115
|
# Header part showing the minimum/maximum number of choices
|
@@ -100,7 +121,28 @@ module TTY
|
|
100
121
|
help = []
|
101
122
|
help << "min. #{@min}" if @min
|
102
123
|
help << "max. #{@max}" if @max
|
103
|
-
"(%s) " % [
|
124
|
+
"(%s) " % [help.join(", ")]
|
125
|
+
end
|
126
|
+
|
127
|
+
# Build a default help text
|
128
|
+
#
|
129
|
+
# @return [String]
|
130
|
+
#
|
131
|
+
# @api private
|
132
|
+
def default_help
|
133
|
+
str = []
|
134
|
+
str << "(Press "
|
135
|
+
str << "#{arrows_help} arrow"
|
136
|
+
str << " or 1-#{choices.size} number" if enumerate?
|
137
|
+
str << " to move, Space"
|
138
|
+
str << "/Ctrl+A|R" if @max.nil?
|
139
|
+
str << " to select"
|
140
|
+
str << " (all|rev)" if @max.nil?
|
141
|
+
str << (filterable? ? "," : " and")
|
142
|
+
str << " Enter to finish"
|
143
|
+
str << " and letters to filter" if filterable?
|
144
|
+
str << ")"
|
145
|
+
str.join
|
104
146
|
end
|
105
147
|
|
106
148
|
# Render initial help text and then currently selected choices
|
@@ -109,19 +151,21 @@ module TTY
|
|
109
151
|
def render_header
|
110
152
|
instructions = @prompt.decorate(help, @help_color)
|
111
153
|
minmax_suffix = @min || @max ? minmax_help : ""
|
154
|
+
print_selected = @selected.size.nonzero? && @echo
|
112
155
|
|
113
156
|
if @done && @echo
|
114
157
|
@prompt.decorate(selected_names, @active_color)
|
115
|
-
elsif @
|
116
|
-
|
117
|
-
minmax_suffix +
|
118
|
-
(
|
119
|
-
|
120
|
-
minmax_suffix + instructions
|
158
|
+
elsif (@first_render && (help_start? || help_always?)) ||
|
159
|
+
(help_always? && !@filter.any? && !@done)
|
160
|
+
minmax_suffix +
|
161
|
+
(print_selected ? "#{selected_names} " : "") +
|
162
|
+
instructions
|
121
163
|
elsif filterable? && @filter.any?
|
122
|
-
minmax_suffix +
|
123
|
-
|
124
|
-
|
164
|
+
minmax_suffix +
|
165
|
+
(print_selected ? "#{selected_names} " : "") +
|
166
|
+
@prompt.decorate(filter_help, @help_color)
|
167
|
+
else
|
168
|
+
minmax_suffix + (print_selected ? selected_names : "")
|
125
169
|
end
|
126
170
|
end
|
127
171
|
|
@@ -144,9 +188,9 @@ module TTY
|
|
144
188
|
|
145
189
|
sync_paginators if @paging_changed
|
146
190
|
paginator.paginate(choices, @active, @per_page) do |choice, index|
|
147
|
-
num = enumerate? ? (index + 1).to_s + @enum +
|
148
|
-
indicator = (index + 1 == @active) ? @symbols[:marker] :
|
149
|
-
indicator +=
|
191
|
+
num = enumerate? ? (index + 1).to_s + @enum + " " : ""
|
192
|
+
indicator = (index + 1 == @active) ? @symbols[:marker] : " "
|
193
|
+
indicator += " "
|
150
194
|
message = if @selected.include?(choice) && !choice.disabled?
|
151
195
|
selected = @prompt.decorate(@symbols[:radio_on], @active_color)
|
152
196
|
"#{selected} #{num}#{choice.name}"
|
@@ -157,7 +201,7 @@ module TTY
|
|
157
201
|
"#{@symbols[:radio_off]} #{num}#{choice.name}"
|
158
202
|
end
|
159
203
|
end_index = paginated? ? paginator.end_index : choices.size - 1
|
160
|
-
newline = (index == end_index) ?
|
204
|
+
newline = (index == end_index) ? "" : "\n"
|
161
205
|
output << indicator + message + newline
|
162
206
|
end
|
163
207
|
|
data/lib/tty/prompt/multiline.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative
|
4
|
-
require_relative
|
3
|
+
require_relative "question"
|
4
|
+
require_relative "symbols"
|
5
5
|
|
6
6
|
module TTY
|
7
7
|
class Prompt
|
@@ -9,7 +9,7 @@ module TTY
|
|
9
9
|
#
|
10
10
|
# @api private
|
11
11
|
class Multiline < Question
|
12
|
-
HELP =
|
12
|
+
HELP = "(Press Ctrl+D or Ctrl+Z to finish)".freeze
|
13
13
|
|
14
14
|
def initialize(prompt, **options)
|
15
15
|
super
|
@@ -55,8 +55,9 @@ module TTY
|
|
55
55
|
@prompt.print(question)
|
56
56
|
@lines = read_input
|
57
57
|
@input = "#{@lines.first.strip} ..." unless @lines.first.to_s.empty?
|
58
|
-
if Utils.blank?(@input)
|
59
|
-
@input = default
|
58
|
+
if Utils.blank?(@input) && default?
|
59
|
+
@input = default
|
60
|
+
@lines = default
|
60
61
|
end
|
61
62
|
@evaluator.(@lines)
|
62
63
|
end
|
data/lib/tty/prompt/paginator.rb
CHANGED
data/lib/tty/prompt/question.rb
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative
|
4
|
-
require_relative
|
5
|
-
require_relative
|
6
|
-
require_relative
|
7
|
-
require_relative
|
8
|
-
require_relative
|
3
|
+
require_relative "converters"
|
4
|
+
require_relative "evaluator"
|
5
|
+
require_relative "question/modifier"
|
6
|
+
require_relative "question/validation"
|
7
|
+
require_relative "question/checks"
|
8
|
+
require_relative "utils"
|
9
9
|
|
10
10
|
module TTY
|
11
11
|
# A class responsible for shell prompt interactions.
|
@@ -35,23 +35,30 @@ module TTY
|
|
35
35
|
#
|
36
36
|
# @api public
|
37
37
|
def initialize(prompt, **options)
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
@
|
45
|
-
@
|
46
|
-
@
|
38
|
+
# Option deprecation
|
39
|
+
if options[:validation]
|
40
|
+
warn "[DEPRECATION] The `:validation` option is deprecated. Use `:validate` instead."
|
41
|
+
options[:validate] = options[:validation]
|
42
|
+
end
|
43
|
+
|
44
|
+
@prompt = prompt
|
45
|
+
@prefix = options.fetch(:prefix) { @prompt.prefix }
|
46
|
+
@default = options.fetch(:default) { UndefinedSetting }
|
47
|
+
@required = options.fetch(:required) { false }
|
48
|
+
@echo = options.fetch(:echo) { true }
|
49
|
+
@in = options.fetch(:in) { UndefinedSetting }
|
50
|
+
@modifier = options.fetch(:modifier) { [] }
|
51
|
+
@validation = options.fetch(:validate) { UndefinedSetting }
|
52
|
+
@convert = options.fetch(:convert) { UndefinedSetting }
|
47
53
|
@active_color = options.fetch(:active_color) { @prompt.active_color }
|
48
|
-
@help_color
|
49
|
-
@error_color
|
50
|
-
@value
|
51
|
-
@
|
52
|
-
@
|
54
|
+
@help_color = options.fetch(:help_color) { @prompt.help_color }
|
55
|
+
@error_color = options.fetch(:error_color) { :red }
|
56
|
+
@value = options.fetch(:value) { UndefinedSetting }
|
57
|
+
@quiet = options.fetch(:quiet) { @prompt.quiet }
|
58
|
+
@messages = Utils.deep_copy(options.fetch(:messages) { { } })
|
59
|
+
@done = false
|
53
60
|
@first_render = true
|
54
|
-
@input
|
61
|
+
@input = nil
|
55
62
|
|
56
63
|
@evaluator = Evaluator.new(self)
|
57
64
|
|
@@ -60,6 +67,7 @@ module TTY
|
|
60
67
|
@evaluator << CheckRange
|
61
68
|
@evaluator << CheckValidation
|
62
69
|
@evaluator << CheckModifier
|
70
|
+
@evaluator << CheckConversion
|
63
71
|
end
|
64
72
|
|
65
73
|
# Stores all the error messages displayed to user
|
@@ -85,7 +93,7 @@ module TTY
|
|
85
93
|
if template && !template.match(/\%\{/).nil?
|
86
94
|
[template % tokens]
|
87
95
|
else
|
88
|
-
[template ||
|
96
|
+
[template || ""]
|
89
97
|
end
|
90
98
|
end
|
91
99
|
|
@@ -96,7 +104,7 @@ module TTY
|
|
96
104
|
# @return [self]
|
97
105
|
#
|
98
106
|
# @api public
|
99
|
-
def call(message =
|
107
|
+
def call(message = "", &block)
|
100
108
|
@message = message
|
101
109
|
block.call(self) if block
|
102
110
|
@prompt.subscribe(self) do
|
@@ -122,8 +130,8 @@ module TTY
|
|
122
130
|
total_lines = @prompt.count_screen_lines(input_line)
|
123
131
|
@prompt.print(refresh(question.lines.count, total_lines))
|
124
132
|
end
|
125
|
-
@prompt.print(render_question)
|
126
|
-
|
133
|
+
@prompt.print(render_question) unless @quiet
|
134
|
+
result.value
|
127
135
|
end
|
128
136
|
|
129
137
|
# Render question
|
@@ -141,7 +149,7 @@ module TTY
|
|
141
149
|
elsif @done
|
142
150
|
header << @prompt.decorate(@input.to_s, @active_color)
|
143
151
|
elsif default? && !Utils.blank?(@default)
|
144
|
-
header << @prompt.decorate("(#{default})", @help_color) +
|
152
|
+
header << @prompt.decorate("(#{default})", @help_color) + " "
|
145
153
|
end
|
146
154
|
header << "\n" if @done
|
147
155
|
header.join
|
@@ -177,7 +185,7 @@ module TTY
|
|
177
185
|
# @api private
|
178
186
|
def render_error(errors)
|
179
187
|
errors.reduce([]) do |acc, err|
|
180
|
-
acc << @prompt.decorate(
|
188
|
+
acc << @prompt.decorate(">>", :red) + " " + err
|
181
189
|
acc
|
182
190
|
end.join("\n")
|
183
191
|
end
|
@@ -211,8 +219,13 @@ module TTY
|
|
211
219
|
#
|
212
220
|
# @api private
|
213
221
|
def convert_result(value)
|
214
|
-
if convert?
|
215
|
-
|
222
|
+
if convert? && !Utils.blank?(value)
|
223
|
+
case @convert
|
224
|
+
when Proc
|
225
|
+
@convert.call(value)
|
226
|
+
else
|
227
|
+
Converters.convert(@convert, value)
|
228
|
+
end
|
216
229
|
else
|
217
230
|
value
|
218
231
|
end
|
@@ -221,8 +234,13 @@ module TTY
|
|
221
234
|
# Specify answer conversion
|
222
235
|
#
|
223
236
|
# @api public
|
224
|
-
def convert(value)
|
225
|
-
|
237
|
+
def convert(value = (not_set = true), message = nil)
|
238
|
+
messages[:convert?] = message if message
|
239
|
+
if not_set
|
240
|
+
@convert
|
241
|
+
else
|
242
|
+
@convert = value
|
243
|
+
end
|
226
244
|
end
|
227
245
|
|
228
246
|
# Check if conversion is set
|
@@ -345,6 +363,13 @@ module TTY
|
|
345
363
|
@in != UndefinedSetting
|
346
364
|
end
|
347
365
|
|
366
|
+
# Set quiet mode.
|
367
|
+
#
|
368
|
+
# @api public
|
369
|
+
def quiet(value)
|
370
|
+
@quiet = value
|
371
|
+
end
|
372
|
+
|
348
373
|
# @api public
|
349
374
|
def to_s
|
350
375
|
message.to_s
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "../const"
|
4
|
+
|
3
5
|
module TTY
|
4
6
|
class Prompt
|
5
7
|
class Question
|
@@ -81,6 +83,22 @@ module TTY
|
|
81
83
|
end
|
82
84
|
end
|
83
85
|
end
|
86
|
+
|
87
|
+
class CheckConversion
|
88
|
+
def self.call(question, value)
|
89
|
+
if question.convert? && !Utils.blank?(value)
|
90
|
+
result = question.convert_result(value)
|
91
|
+
if result == Const::Undefined
|
92
|
+
tokens = {value: value, type: question.convert}
|
93
|
+
[value, question.message_for(:convert?, tokens)]
|
94
|
+
else
|
95
|
+
[result]
|
96
|
+
end
|
97
|
+
else
|
98
|
+
[value]
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
84
102
|
end # Checks
|
85
103
|
end # Question
|
86
104
|
end # Prompt
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TTY
|
4
|
+
class Prompt
|
5
|
+
# @api private
|
6
|
+
class SelectedChoices
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
attr_reader :size
|
10
|
+
|
11
|
+
# Create selected choices
|
12
|
+
#
|
13
|
+
# @param [Array<Choice>] selected
|
14
|
+
# @param [Array<Integer>] indexes
|
15
|
+
#
|
16
|
+
# @api public
|
17
|
+
def initialize(selected = [], indexes = [])
|
18
|
+
@selected = selected
|
19
|
+
@indexes = indexes
|
20
|
+
@size = @selected.size
|
21
|
+
end
|
22
|
+
|
23
|
+
# Clear selected choices
|
24
|
+
#
|
25
|
+
# @api public
|
26
|
+
def clear
|
27
|
+
@indexes.clear
|
28
|
+
@selected.clear
|
29
|
+
@size = 0
|
30
|
+
end
|
31
|
+
|
32
|
+
# Iterate over selected choices
|
33
|
+
#
|
34
|
+
# @api public
|
35
|
+
def each(&block)
|
36
|
+
return to_enum unless block_given?
|
37
|
+
@selected.each(&block)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Insert choice at index
|
41
|
+
#
|
42
|
+
# @param [Integer] index
|
43
|
+
# @param [Choice] choice
|
44
|
+
#
|
45
|
+
# @api public
|
46
|
+
def insert(index, choice)
|
47
|
+
insert_idx = find_index_by { |i| index < @indexes[i] }
|
48
|
+
insert_idx ||= -1
|
49
|
+
@indexes.insert(insert_idx, index)
|
50
|
+
@selected.insert(insert_idx, choice)
|
51
|
+
@size += 1
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
# Delete choice at index
|
56
|
+
#
|
57
|
+
# @return [Choice]
|
58
|
+
# the deleted choice
|
59
|
+
#
|
60
|
+
# @api public
|
61
|
+
def delete_at(index)
|
62
|
+
delete_idx = @indexes.each_index.find { |i| index == @indexes[i] }
|
63
|
+
return nil unless delete_idx
|
64
|
+
|
65
|
+
@indexes.delete_at(delete_idx)
|
66
|
+
choice = @selected.delete_at(delete_idx)
|
67
|
+
@size -= 1
|
68
|
+
choice
|
69
|
+
end
|
70
|
+
|
71
|
+
def find_index_by(&search)
|
72
|
+
(0...@size).bsearch(&search)
|
73
|
+
end
|
74
|
+
end # SelectedChoices
|
75
|
+
end # Prompt
|
76
|
+
end # TTY
|