tty2-prompt 0.23.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +14 -0
- data/LICENSE.txt +23 -0
- data/README.md +52 -0
- data/lib/tty2/prompt/answers_collector.rb +78 -0
- data/lib/tty2/prompt/block_paginator.rb +59 -0
- data/lib/tty2/prompt/choice.rb +147 -0
- data/lib/tty2/prompt/choices.rb +129 -0
- data/lib/tty2/prompt/confirm_question.rb +158 -0
- data/lib/tty2/prompt/const.rb +17 -0
- data/lib/tty2/prompt/converter_dsl.rb +21 -0
- data/lib/tty2/prompt/converter_registry.rb +69 -0
- data/lib/tty2/prompt/converters.rb +182 -0
- data/lib/tty2/prompt/distance.rb +49 -0
- data/lib/tty2/prompt/enum_list.rb +433 -0
- data/lib/tty2/prompt/errors.rb +31 -0
- data/lib/tty2/prompt/evaluator.rb +29 -0
- data/lib/tty2/prompt/expander.rb +321 -0
- data/lib/tty2/prompt/keypress.rb +98 -0
- data/lib/tty2/prompt/list.rb +589 -0
- data/lib/tty2/prompt/mask_question.rb +96 -0
- data/lib/tty2/prompt/multi_list.rb +224 -0
- data/lib/tty2/prompt/multiline.rb +72 -0
- data/lib/tty2/prompt/paginator.rb +111 -0
- data/lib/tty2/prompt/question/checks.rb +105 -0
- data/lib/tty2/prompt/question/modifier.rb +96 -0
- data/lib/tty2/prompt/question/validation.rb +72 -0
- data/lib/tty2/prompt/question.rb +391 -0
- data/lib/tty2/prompt/result.rb +42 -0
- data/lib/tty2/prompt/selected_choices.rb +77 -0
- data/lib/tty2/prompt/slider.rb +286 -0
- data/lib/tty2/prompt/statement.rb +55 -0
- data/lib/tty2/prompt/suggestion.rb +113 -0
- data/lib/tty2/prompt/symbols.rb +89 -0
- data/lib/tty2/prompt/test.rb +36 -0
- data/lib/tty2/prompt/timer.rb +75 -0
- data/lib/tty2/prompt/utils.rb +42 -0
- data/lib/tty2/prompt/version.rb +7 -0
- data/lib/tty2/prompt.rb +589 -0
- data/lib/tty2-prompt.rb +1 -0
- metadata +148 -0
@@ -0,0 +1,224 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "list"
|
4
|
+
require_relative "selected_choices"
|
5
|
+
|
6
|
+
module TTY2
|
7
|
+
class Prompt
|
8
|
+
# A class responsible for rendering multi select list menu.
|
9
|
+
# Used by {Prompt} to display interactive choice menu.
|
10
|
+
#
|
11
|
+
# @api private
|
12
|
+
class MultiList < List
|
13
|
+
# Create instance of TTY2::Prompt::MultiList menu.
|
14
|
+
#
|
15
|
+
# @param [Prompt] :prompt
|
16
|
+
# @param [Hash] options
|
17
|
+
#
|
18
|
+
# @api public
|
19
|
+
def initialize(prompt, **options)
|
20
|
+
super
|
21
|
+
@selected = SelectedChoices.new
|
22
|
+
@help = options[:help]
|
23
|
+
@echo = options.fetch(:echo, true)
|
24
|
+
@min = options[:min]
|
25
|
+
@max = options[:max]
|
26
|
+
end
|
27
|
+
|
28
|
+
# Set a minimum number of choices
|
29
|
+
#
|
30
|
+
# @api public
|
31
|
+
def min(value)
|
32
|
+
@min = value
|
33
|
+
end
|
34
|
+
|
35
|
+
# Set a maximum number of choices
|
36
|
+
#
|
37
|
+
# @api public
|
38
|
+
def max(value)
|
39
|
+
@max = value
|
40
|
+
end
|
41
|
+
|
42
|
+
# Callback fired when enter/return key is pressed
|
43
|
+
#
|
44
|
+
# @api private
|
45
|
+
def keyenter(*)
|
46
|
+
valid = true
|
47
|
+
valid = @min <= @selected.size if @min
|
48
|
+
valid = @selected.size <= @max if @max
|
49
|
+
|
50
|
+
super if valid
|
51
|
+
end
|
52
|
+
alias keyreturn keyenter
|
53
|
+
|
54
|
+
# Callback fired when space key is pressed
|
55
|
+
#
|
56
|
+
# @api private
|
57
|
+
def keyspace(*)
|
58
|
+
active_choice = choices[@active - 1]
|
59
|
+
if @selected.include?(active_choice)
|
60
|
+
@selected.delete_at(@active - 1)
|
61
|
+
else
|
62
|
+
return if @max && @selected.size >= @max
|
63
|
+
|
64
|
+
@selected.insert(@active - 1, active_choice)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Selects all choices when Ctrl+A is pressed
|
69
|
+
#
|
70
|
+
# @api private
|
71
|
+
def keyctrl_a(*)
|
72
|
+
return if @max && @max < choices.size
|
73
|
+
|
74
|
+
@selected = SelectedChoices.new(choices.enabled, choices.enabled_indexes)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Revert currently selected choices when Ctrl+I is pressed
|
78
|
+
#
|
79
|
+
# @api private
|
80
|
+
def keyctrl_r(*)
|
81
|
+
return if @max && @max < choices.size
|
82
|
+
|
83
|
+
indexes = choices.each_with_index.reduce([]) do |acc, (choice, idx)|
|
84
|
+
acc << idx if !choice.disabled? && !@selected.include?(choice)
|
85
|
+
acc
|
86
|
+
end
|
87
|
+
@selected = SelectedChoices.new(choices.enabled - @selected.to_a, indexes)
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
# Setup default options and active selection
|
93
|
+
#
|
94
|
+
# @api private
|
95
|
+
def setup_defaults
|
96
|
+
validate_defaults
|
97
|
+
# At this stage, @choices matches all the visible choices.
|
98
|
+
default_indexes = @default.map do |d|
|
99
|
+
if d.to_s =~ INTEGER_MATCHER
|
100
|
+
d - 1
|
101
|
+
else
|
102
|
+
choices.index(choices.find_by(:name, d.to_s))
|
103
|
+
end
|
104
|
+
end
|
105
|
+
@selected = SelectedChoices.new(@choices.values_at(*default_indexes),
|
106
|
+
default_indexes)
|
107
|
+
|
108
|
+
if @default.empty?
|
109
|
+
# no default, pick the first non-disabled choice
|
110
|
+
@active = choices.index { |choice| !choice.disabled? } + 1
|
111
|
+
elsif @default.last.to_s =~ INTEGER_MATCHER
|
112
|
+
@active = @default.last
|
113
|
+
elsif default_choice = choices.find_by(:name, @default.last.to_s)
|
114
|
+
@active = choices.index(default_choice) + 1
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Generate selected items names
|
119
|
+
#
|
120
|
+
# @return [String]
|
121
|
+
#
|
122
|
+
# @api private
|
123
|
+
def selected_names
|
124
|
+
@selected.map(&:name).join(", ")
|
125
|
+
end
|
126
|
+
|
127
|
+
# Header part showing the minimum/maximum number of choices
|
128
|
+
#
|
129
|
+
# @return [String]
|
130
|
+
#
|
131
|
+
# @api private
|
132
|
+
def minmax_help
|
133
|
+
help = []
|
134
|
+
help << "min. #{@min}" if @min
|
135
|
+
help << "max. #{@max}" if @max
|
136
|
+
"(%s) " % [help.join(", ")]
|
137
|
+
end
|
138
|
+
|
139
|
+
# Build a default help text
|
140
|
+
#
|
141
|
+
# @return [String]
|
142
|
+
#
|
143
|
+
# @api private
|
144
|
+
def default_help
|
145
|
+
str = []
|
146
|
+
str << "(Press "
|
147
|
+
str << "#{arrows_help} arrow"
|
148
|
+
str << " or 1-#{choices.size} number" if enumerate?
|
149
|
+
str << " to move, Space"
|
150
|
+
str << "/Ctrl+A|R" if @max.nil?
|
151
|
+
str << " to select"
|
152
|
+
str << " (all|rev)" if @max.nil?
|
153
|
+
str << (filterable? ? "," : " and")
|
154
|
+
str << " Enter to finish"
|
155
|
+
str << " and letters to filter" if filterable?
|
156
|
+
str << ")"
|
157
|
+
str.join
|
158
|
+
end
|
159
|
+
|
160
|
+
# Render initial help text and then currently selected choices
|
161
|
+
#
|
162
|
+
# @api private
|
163
|
+
def render_header
|
164
|
+
instructions = @prompt.decorate(help, @help_color)
|
165
|
+
minmax_suffix = @min || @max ? minmax_help : ""
|
166
|
+
print_selected = @selected.size.nonzero? && @echo
|
167
|
+
|
168
|
+
if @done && @echo
|
169
|
+
@prompt.decorate(selected_names, @active_color)
|
170
|
+
elsif (@first_render && (help_start? || help_always?)) ||
|
171
|
+
(help_always? && !@filter.any? && !@done)
|
172
|
+
minmax_suffix +
|
173
|
+
(print_selected ? "#{selected_names} " : "") +
|
174
|
+
instructions
|
175
|
+
elsif filterable? && @filter.any?
|
176
|
+
minmax_suffix +
|
177
|
+
(print_selected ? "#{selected_names} " : "") +
|
178
|
+
@prompt.decorate(filter_help, @help_color)
|
179
|
+
else
|
180
|
+
minmax_suffix + (print_selected ? selected_names : "")
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# All values for the choices selected
|
185
|
+
#
|
186
|
+
# @return [Array[nil,Object]]
|
187
|
+
#
|
188
|
+
# @api private
|
189
|
+
def answer
|
190
|
+
@selected.map(&:value)
|
191
|
+
end
|
192
|
+
|
193
|
+
# Render menu with choices to select from
|
194
|
+
#
|
195
|
+
# @return [String]
|
196
|
+
#
|
197
|
+
# @api private
|
198
|
+
def render_menu
|
199
|
+
output = []
|
200
|
+
|
201
|
+
sync_paginators if @paging_changed
|
202
|
+
paginator.paginate(choices, @active, @per_page) do |choice, index|
|
203
|
+
num = enumerate? ? (index + 1).to_s + @enum + " " : ""
|
204
|
+
indicator = (index + 1 == @active) ? @symbols[:marker] : " "
|
205
|
+
indicator += " "
|
206
|
+
message = if @selected.include?(choice) && !choice.disabled?
|
207
|
+
selected = @prompt.decorate(@symbols[:radio_on], @active_color)
|
208
|
+
"#{selected} #{num}#{choice.name}"
|
209
|
+
elsif choice.disabled?
|
210
|
+
@prompt.decorate(@symbols[:cross], :red) +
|
211
|
+
" #{num}#{choice.name} #{choice.disabled}"
|
212
|
+
else
|
213
|
+
"#{@symbols[:radio_off]} #{num}#{choice.name}"
|
214
|
+
end
|
215
|
+
end_index = paginated? ? paginator.end_index : choices.size - 1
|
216
|
+
newline = (index == end_index) ? "" : "\n"
|
217
|
+
output << indicator + message + newline
|
218
|
+
end
|
219
|
+
|
220
|
+
output.join
|
221
|
+
end
|
222
|
+
end # MultiList
|
223
|
+
end # Prompt
|
224
|
+
end # TTY2
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "question"
|
4
|
+
require_relative "symbols"
|
5
|
+
|
6
|
+
module TTY2
|
7
|
+
class Prompt
|
8
|
+
# A prompt responsible for multi line user input
|
9
|
+
#
|
10
|
+
# @api private
|
11
|
+
class Multiline < Question
|
12
|
+
HELP = "(Press Ctrl+D or Ctrl+Z to finish)".freeze
|
13
|
+
|
14
|
+
def initialize(prompt, **options)
|
15
|
+
super
|
16
|
+
@help = options[:help] || self.class::HELP
|
17
|
+
@first_render = true
|
18
|
+
@lines_count = 0
|
19
|
+
end
|
20
|
+
|
21
|
+
# Provide help information
|
22
|
+
#
|
23
|
+
# @return [String]
|
24
|
+
#
|
25
|
+
# @api public
|
26
|
+
def help(value = (not_set = true))
|
27
|
+
return @help if not_set
|
28
|
+
|
29
|
+
@help = value
|
30
|
+
end
|
31
|
+
|
32
|
+
def read_input
|
33
|
+
@prompt.read_multiline
|
34
|
+
end
|
35
|
+
|
36
|
+
def keyreturn(*)
|
37
|
+
@lines_count += 1
|
38
|
+
end
|
39
|
+
alias keyenter keyreturn
|
40
|
+
|
41
|
+
def render_question
|
42
|
+
header = ["#{@prefix}#{message} "]
|
43
|
+
if !echo?
|
44
|
+
header
|
45
|
+
elsif @done
|
46
|
+
header << @prompt.decorate(@input.to_s, @active_color)
|
47
|
+
elsif @first_render
|
48
|
+
header << @prompt.decorate(help, @help_color)
|
49
|
+
@first_render = false
|
50
|
+
end
|
51
|
+
header << "\n"
|
52
|
+
header.join
|
53
|
+
end
|
54
|
+
|
55
|
+
def process_input(question)
|
56
|
+
@prompt.print(question)
|
57
|
+
@lines = read_input
|
58
|
+
@input = "#{@lines.first.strip} ..." unless @lines.first.to_s.empty?
|
59
|
+
if Utils.blank?(@input) && default?
|
60
|
+
@input = default
|
61
|
+
@lines = default
|
62
|
+
end
|
63
|
+
@evaluator.(@lines)
|
64
|
+
end
|
65
|
+
|
66
|
+
def refresh(lines, lines_to_clear)
|
67
|
+
size = @lines_count + lines_to_clear + 1
|
68
|
+
@prompt.clear_lines(size)
|
69
|
+
end
|
70
|
+
end # Multiline
|
71
|
+
end # Prompt
|
72
|
+
end # TTY2
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TTY2
|
4
|
+
class Prompt
|
5
|
+
class Paginator
|
6
|
+
DEFAULT_PAGE_SIZE = 6
|
7
|
+
|
8
|
+
# The 0-based index of the first item on this page
|
9
|
+
attr_accessor :start_index
|
10
|
+
|
11
|
+
# The 0-based index of the last item on this page
|
12
|
+
attr_reader :end_index
|
13
|
+
|
14
|
+
# The 0-based index of the active item on this page
|
15
|
+
attr_reader :current_index
|
16
|
+
|
17
|
+
# The 0-based index of the previously active item on this page
|
18
|
+
attr_reader :last_index
|
19
|
+
|
20
|
+
# Create a Paginator
|
21
|
+
#
|
22
|
+
# @api private
|
23
|
+
def initialize(**options)
|
24
|
+
@last_index = Array(options[:default]).flatten.first || 0
|
25
|
+
@per_page = options[:per_page]
|
26
|
+
@start_index = Array(options[:default]).flatten.first
|
27
|
+
end
|
28
|
+
|
29
|
+
# Reset current page indexes
|
30
|
+
#
|
31
|
+
# @api private
|
32
|
+
def reset!
|
33
|
+
@start_index = nil
|
34
|
+
@end_index = nil
|
35
|
+
end
|
36
|
+
|
37
|
+
# Check if page size is valid
|
38
|
+
#
|
39
|
+
# @raise [InvalidArgument]
|
40
|
+
#
|
41
|
+
# @api private
|
42
|
+
def check_page_size!
|
43
|
+
raise InvalidArgument, "per_page must be > 0" if @per_page < 1
|
44
|
+
end
|
45
|
+
|
46
|
+
# Paginate collection given an active index
|
47
|
+
#
|
48
|
+
# @param [Array[Choice]] list
|
49
|
+
# a collection of choice items
|
50
|
+
# @param [Integer] active
|
51
|
+
# current choice active index
|
52
|
+
# @param [Integer] per_page
|
53
|
+
# number of choice items per page
|
54
|
+
#
|
55
|
+
# @return [Enumerable]
|
56
|
+
# the list between start and end index
|
57
|
+
#
|
58
|
+
# @api public
|
59
|
+
def paginate(list, active, per_page = nil, &block)
|
60
|
+
current_index = active - 1
|
61
|
+
default_size = (list.size <= DEFAULT_PAGE_SIZE ? list.size : DEFAULT_PAGE_SIZE)
|
62
|
+
@per_page = @per_page || per_page || default_size
|
63
|
+
check_page_size!
|
64
|
+
@start_index ||= (current_index / @per_page) * @per_page
|
65
|
+
@end_index ||= @start_index + @per_page - 1
|
66
|
+
|
67
|
+
# Don't paginate short lists
|
68
|
+
if list.size <= @per_page
|
69
|
+
@start_index = 0
|
70
|
+
@end_index = list.size - 1
|
71
|
+
if block
|
72
|
+
return list.each_with_index(&block)
|
73
|
+
else
|
74
|
+
return list.each_with_index.to_enum
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
step = (current_index - @last_index).abs
|
79
|
+
if current_index > @last_index # going up
|
80
|
+
if current_index >= @end_index && current_index < list.size - 1
|
81
|
+
last_page = list.size - @per_page
|
82
|
+
@start_index = [@start_index + step, last_page].min
|
83
|
+
end
|
84
|
+
elsif current_index < @last_index # going down
|
85
|
+
if current_index <= @start_index && current_index > 0
|
86
|
+
@start_index = [@start_index - step, 0].max
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Cycle list
|
91
|
+
if current_index.zero?
|
92
|
+
@start_index = 0
|
93
|
+
elsif current_index == list.size - 1
|
94
|
+
@start_index = list.size - 1 - (@per_page - 1)
|
95
|
+
end
|
96
|
+
|
97
|
+
@end_index = @start_index + (@per_page - 1)
|
98
|
+
@last_index = current_index
|
99
|
+
|
100
|
+
sliced_list = list[@start_index..@end_index]
|
101
|
+
page_range = (@start_index..@end_index)
|
102
|
+
|
103
|
+
return sliced_list.zip(page_range).to_enum unless block_given?
|
104
|
+
|
105
|
+
sliced_list.each_with_index do |item, index|
|
106
|
+
block[item, @start_index + index]
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end # Paginator
|
110
|
+
end # Prompt
|
111
|
+
end # TTY2
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../const"
|
4
|
+
|
5
|
+
module TTY2
|
6
|
+
class Prompt
|
7
|
+
class Question
|
8
|
+
module Checks
|
9
|
+
# Check if modifications are applicable
|
10
|
+
class CheckModifier
|
11
|
+
def self.call(question, value)
|
12
|
+
if !question.modifier.nil? || question.modifier
|
13
|
+
[Modifier.new(question.modifier).apply_to(value)]
|
14
|
+
else
|
15
|
+
[value]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Check if value is within range
|
21
|
+
class CheckRange
|
22
|
+
def self.float?(value)
|
23
|
+
!/[-+]?(\d*[.])?\d+/.match(value.to_s).nil?
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.int?(value)
|
27
|
+
!/^[-+]?\d+$/.match(value.to_s).nil?
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.cast(value)
|
31
|
+
if float?(value)
|
32
|
+
value.to_f
|
33
|
+
elsif int?(value)
|
34
|
+
value.to_i
|
35
|
+
else
|
36
|
+
value
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.call(question, value)
|
41
|
+
if !question.in? ||
|
42
|
+
(question.in? && question.in.include?(cast(value)))
|
43
|
+
[value]
|
44
|
+
else
|
45
|
+
tokens = { value: value, in: question.in }
|
46
|
+
[value, question.message_for(:range?, tokens)]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Check if input requires validation
|
52
|
+
class CheckValidation
|
53
|
+
def self.call(question, value)
|
54
|
+
if !question.validation? || (question.required? && value.nil?) ||
|
55
|
+
(question.validation? &&
|
56
|
+
Validation.new(question.validation).call(value))
|
57
|
+
[value]
|
58
|
+
else
|
59
|
+
tokens = { valid: question.validation.inspect, value: value }
|
60
|
+
[value, question.message_for(:valid?, tokens)]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Check if default value provided
|
66
|
+
class CheckDefault
|
67
|
+
def self.call(question, value)
|
68
|
+
if value.nil? && question.default?
|
69
|
+
[question.default]
|
70
|
+
else
|
71
|
+
[value]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Check if input is required
|
77
|
+
class CheckRequired
|
78
|
+
def self.call(question, value)
|
79
|
+
if question.required? && !question.default? && value.nil?
|
80
|
+
[value, question.message_for(:required?)]
|
81
|
+
else
|
82
|
+
[value]
|
83
|
+
end
|
84
|
+
end
|
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
|
102
|
+
end # Checks
|
103
|
+
end # Question
|
104
|
+
end # Prompt
|
105
|
+
end # TTY2
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TTY2
|
4
|
+
class Prompt
|
5
|
+
class Question
|
6
|
+
# A class representing String modifications.
|
7
|
+
class Modifier
|
8
|
+
attr_reader :modifiers
|
9
|
+
|
10
|
+
# Initialize a Modifier
|
11
|
+
#
|
12
|
+
# @api public
|
13
|
+
def initialize(modifiers)
|
14
|
+
@modifiers = modifiers
|
15
|
+
end
|
16
|
+
|
17
|
+
# Change supplied value according to the given string transformation.
|
18
|
+
# Valid settings are:
|
19
|
+
#
|
20
|
+
# @param [String] value
|
21
|
+
# the string to be modified
|
22
|
+
#
|
23
|
+
# @return [String]
|
24
|
+
#
|
25
|
+
# @api private
|
26
|
+
def apply_to(value)
|
27
|
+
modifiers.reduce(value) do |result, mod|
|
28
|
+
result = Modifier.letter_case(mod, result)
|
29
|
+
Modifier.whitespace(mod, result)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Changes letter casing in a string according to valid modifications.
|
34
|
+
# For invalid modification option the string is preserved.
|
35
|
+
#
|
36
|
+
# @param [Symbol] mod
|
37
|
+
# the modification to change the string
|
38
|
+
#
|
39
|
+
# @option mod [Symbol] :up change to upper case
|
40
|
+
# @option mod [Symbol] :upcase change to upper case
|
41
|
+
# @option mod [Symbol] :uppercase change to upper case
|
42
|
+
# @option mod [Symbol] :down change to lower case
|
43
|
+
# @option mod [Symbol] :downcase change to lower case
|
44
|
+
# @option mod [Symbol] :capitalize change all words to start
|
45
|
+
# with uppercase case letter
|
46
|
+
#
|
47
|
+
# @return [String]
|
48
|
+
#
|
49
|
+
# @api public
|
50
|
+
def self.letter_case(mod, value)
|
51
|
+
return value unless value.is_a?(String)
|
52
|
+
|
53
|
+
case mod
|
54
|
+
when :up, :upcase, :uppercase
|
55
|
+
value.upcase
|
56
|
+
when :down, :downcase, :lowercase
|
57
|
+
value.downcase
|
58
|
+
when :capitalize
|
59
|
+
value.capitalize
|
60
|
+
else
|
61
|
+
value
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Changes whitespace in a string according to valid modifications.
|
66
|
+
#
|
67
|
+
# @param [Symbol] mod
|
68
|
+
# the modification to change the string
|
69
|
+
#
|
70
|
+
# @option mod [String] :trim, :strip
|
71
|
+
# remove whitespace for the start and end
|
72
|
+
# @option mod [String] :chomp remove record separator from the end
|
73
|
+
# @option mod [String] :collapse remove any duplicate whitespace
|
74
|
+
# @option mod [String] :remove remove all whitespace
|
75
|
+
#
|
76
|
+
# @api public
|
77
|
+
def self.whitespace(mod, value)
|
78
|
+
return value unless value.is_a?(String)
|
79
|
+
|
80
|
+
case mod
|
81
|
+
when :trim, :strip
|
82
|
+
value.strip
|
83
|
+
when :chomp
|
84
|
+
value.chomp
|
85
|
+
when :collapse
|
86
|
+
value.gsub(/\s+/, " ")
|
87
|
+
when :remove
|
88
|
+
value.gsub(/\s+/, "")
|
89
|
+
else
|
90
|
+
value
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end # Modifier
|
94
|
+
end # Question
|
95
|
+
end # Prompt
|
96
|
+
end # TTY2
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TTY2
|
4
|
+
class Prompt
|
5
|
+
class Question
|
6
|
+
# A class representing question validation.
|
7
|
+
class Validation
|
8
|
+
# Available validator names
|
9
|
+
VALIDATORS = {
|
10
|
+
email: /^[a-z0-9._%+-]+@([a-z0-9-]+\.)+[a-z]{2,6}$/i
|
11
|
+
}.freeze
|
12
|
+
|
13
|
+
attr_reader :pattern
|
14
|
+
|
15
|
+
# Initialize a Validation
|
16
|
+
#
|
17
|
+
# @param [Object] pattern
|
18
|
+
#
|
19
|
+
# @return [undefined]
|
20
|
+
#
|
21
|
+
# @api private
|
22
|
+
def initialize(pattern)
|
23
|
+
@pattern = coerce(pattern)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Convert validation into known type.
|
27
|
+
#
|
28
|
+
# @param [Object] pattern
|
29
|
+
#
|
30
|
+
# @raise [TTY2::ValidationCoercion]
|
31
|
+
# raised when failed to convert validation
|
32
|
+
#
|
33
|
+
# @api private
|
34
|
+
def coerce(pattern)
|
35
|
+
case pattern
|
36
|
+
when String, Symbol, Proc
|
37
|
+
pattern
|
38
|
+
when Regexp
|
39
|
+
Regexp.new(pattern.to_s)
|
40
|
+
else
|
41
|
+
raise ValidationCoercion, "Wrong type, got #{pattern.class}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Test if the input passes the validation
|
46
|
+
#
|
47
|
+
# @example
|
48
|
+
# Validation.new(/pattern/)
|
49
|
+
# validation.call(input) # => true
|
50
|
+
#
|
51
|
+
# @param [Object] input
|
52
|
+
# the input to validate
|
53
|
+
#
|
54
|
+
# @return [Boolean]
|
55
|
+
#
|
56
|
+
# @api public
|
57
|
+
def call(input)
|
58
|
+
if pattern.is_a?(String) || pattern.is_a?(Symbol)
|
59
|
+
VALIDATORS.key?(pattern.to_sym)
|
60
|
+
!VALIDATORS[pattern.to_sym].match(input.to_s).nil?
|
61
|
+
elsif pattern.is_a?(Regexp)
|
62
|
+
!pattern.match(input.to_s).nil?
|
63
|
+
elsif pattern.is_a?(Proc)
|
64
|
+
result = pattern.call(input.to_s)
|
65
|
+
result.nil? ? false : result
|
66
|
+
else false
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end # Validation
|
70
|
+
end # Question
|
71
|
+
end # Prompt
|
72
|
+
end # TTY2
|