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
@@ -25,7 +25,7 @@ module TTY
|
|
25
25
|
# Create answer entry
|
26
26
|
#
|
27
27
|
# @example
|
28
|
-
# key(:name).ask(
|
28
|
+
# key(:name).ask("Name?")
|
29
29
|
#
|
30
30
|
# @api public
|
31
31
|
def key(name, &block)
|
@@ -40,7 +40,7 @@ module TTY
|
|
40
40
|
# Change to collect all values for a key
|
41
41
|
#
|
42
42
|
# @example
|
43
|
-
# key(:colors).values.ask(
|
43
|
+
# key(:colors).values.ask("Color?")
|
44
44
|
#
|
45
45
|
# @api public
|
46
46
|
def values(&block)
|
data/lib/tty/prompt/choice.rb
CHANGED
@@ -15,7 +15,7 @@ module TTY
|
|
15
15
|
# Choice.from([:foo, 1])
|
16
16
|
# # => <TTY::Prompt::Choice @key=nil @name="foo" @value=1 @disabled=false>
|
17
17
|
#
|
18
|
-
# Choice.from({name: :foo, value: 1, key:
|
18
|
+
# Choice.from({name: :foo, value: 1, key: "f"}
|
19
19
|
# # => <TTY::Prompt::Choice @key="f" @name="foo" @value=1 @disabled=false>
|
20
20
|
#
|
21
21
|
# @param [Object] val
|
data/lib/tty/prompt/choices.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "forwardable"
|
4
4
|
|
5
|
-
require_relative
|
5
|
+
require_relative "choice"
|
6
6
|
|
7
7
|
module TTY
|
8
8
|
class Prompt
|
@@ -13,15 +13,8 @@ module TTY
|
|
13
13
|
include Enumerable
|
14
14
|
extend Forwardable
|
15
15
|
|
16
|
-
# The actual collection choices
|
17
|
-
#
|
18
|
-
# @return [Array[Choice]]
|
19
|
-
#
|
20
|
-
# @api public
|
21
|
-
attr_reader :choices
|
22
|
-
|
23
16
|
def_delegators :choices, :length, :size, :to_ary, :empty?,
|
24
|
-
|
17
|
+
:values_at, :index, :==
|
25
18
|
|
26
19
|
# Convenience for creating choices
|
27
20
|
#
|
@@ -48,6 +41,20 @@ module TTY
|
|
48
41
|
end
|
49
42
|
end
|
50
43
|
|
44
|
+
# Scope of choices which are not disabled
|
45
|
+
#
|
46
|
+
# @api public
|
47
|
+
def enabled
|
48
|
+
reject(&:disabled?)
|
49
|
+
end
|
50
|
+
|
51
|
+
def enabled_indexes
|
52
|
+
each_with_index.reduce([]) do |acc, (choice, idx)|
|
53
|
+
acc << idx unless choice.disabled?
|
54
|
+
acc
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
51
58
|
# Iterate over all choices in the collection
|
52
59
|
#
|
53
60
|
# @yield [Choice]
|
@@ -94,7 +101,7 @@ module TTY
|
|
94
101
|
# Find a matching choice
|
95
102
|
#
|
96
103
|
# @exmaple
|
97
|
-
# choices.find_by(:name,
|
104
|
+
# choices.find_by(:name, "small")
|
98
105
|
#
|
99
106
|
# @param [Symbol] attr
|
100
107
|
# the attribute name
|
@@ -106,6 +113,16 @@ module TTY
|
|
106
113
|
def find_by(attr, value)
|
107
114
|
find { |choice| choice.public_send(attr) == value }
|
108
115
|
end
|
116
|
+
|
117
|
+
protected
|
118
|
+
|
119
|
+
# The actual collection choices
|
120
|
+
#
|
121
|
+
# @return [Array[Choice]]
|
122
|
+
#
|
123
|
+
# @api private
|
124
|
+
|
125
|
+
attr_reader :choices
|
109
126
|
end # Choices
|
110
127
|
end # Prompt
|
111
128
|
end # TTY
|
@@ -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 "utils"
|
5
5
|
|
6
6
|
module TTY
|
7
7
|
class Prompt
|
@@ -73,9 +73,9 @@ module TTY
|
|
73
73
|
def render_question
|
74
74
|
header = "#{@prefix}#{message} "
|
75
75
|
if !@done
|
76
|
-
header += @prompt.decorate("(#{@suffix})", @help_color) +
|
76
|
+
header += @prompt.decorate("(#{@suffix})", @help_color) + " "
|
77
77
|
else
|
78
|
-
answer =
|
78
|
+
answer = conversion.call(@input)
|
79
79
|
label = answer ? @positive : @negative
|
80
80
|
header += @prompt.decorate(label, @active_color)
|
81
81
|
end
|
@@ -98,11 +98,12 @@ module TTY
|
|
98
98
|
|
99
99
|
# @api private
|
100
100
|
def setup_defaults
|
101
|
+
infer_default
|
101
102
|
@convert = conversion
|
102
103
|
return if suffix? && positive?
|
103
104
|
|
104
105
|
if suffix? && (!positive? || !negative?)
|
105
|
-
parts = @suffix.split(
|
106
|
+
parts = @suffix.split("/")
|
106
107
|
@positive = parts[0]
|
107
108
|
@negative = parts[1]
|
108
109
|
elsif !suffix? && positive?
|
@@ -112,18 +113,28 @@ module TTY
|
|
112
113
|
end
|
113
114
|
end
|
114
115
|
|
116
|
+
# @api private
|
117
|
+
def infer_default
|
118
|
+
converted = Converters.convert(:bool, default.to_s)
|
119
|
+
if converted == Const::Undefined
|
120
|
+
raise InvalidArgument, "default needs to be `true` or `false`"
|
121
|
+
else
|
122
|
+
default(converted)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
115
126
|
# @api private
|
116
127
|
def create_default_labels
|
117
|
-
@suffix = default ?
|
118
|
-
@positive = default ?
|
119
|
-
@negative = default ?
|
128
|
+
@suffix = default ? "Y/n" : "y/N"
|
129
|
+
@positive = default ? "Yes" : "yes"
|
130
|
+
@negative = default ? "no" : "No"
|
120
131
|
@validation = /^(y(es)?|no?)$/i
|
121
132
|
@messages[:valid?] = "Invalid input."
|
122
133
|
end
|
123
134
|
|
124
135
|
# @api private
|
125
136
|
def create_suffix
|
126
|
-
(default ? positive.capitalize : positive.downcase) +
|
137
|
+
(default ? positive.capitalize : positive.downcase) + "/" +
|
127
138
|
(default ? negative.downcase : negative.capitalize)
|
128
139
|
end
|
129
140
|
|
@@ -131,12 +142,12 @@ module TTY
|
|
131
142
|
#
|
132
143
|
# @api private
|
133
144
|
def conversion
|
134
|
-
|
145
|
+
->(input) do
|
135
146
|
positive_word = Regexp.escape(positive)
|
136
147
|
positive_letter = Regexp.escape(positive[0])
|
137
|
-
pattern = Regexp.new("
|
148
|
+
pattern = Regexp.new("^(#{positive_word}|#{positive_letter})$", true)
|
138
149
|
!input.match(pattern).nil?
|
139
|
-
|
150
|
+
end
|
140
151
|
end
|
141
152
|
end # ConfirmQuestion
|
142
153
|
end # Prompt
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TTY
|
4
|
+
class Prompt
|
5
|
+
module Const
|
6
|
+
Undefined = Object.new.tap do |obj|
|
7
|
+
def obj.to_s
|
8
|
+
"undefined"
|
9
|
+
end
|
10
|
+
|
11
|
+
def obj.inspect
|
12
|
+
"undefined".inspect
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end # Const
|
16
|
+
end # Prompt
|
17
|
+
end # TTY
|
@@ -1,21 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative
|
3
|
+
require_relative "converter_registry"
|
4
4
|
|
5
5
|
module TTY
|
6
6
|
class Prompt
|
7
7
|
module ConverterDSL
|
8
8
|
def converter_registry
|
9
|
-
@
|
9
|
+
@__converter_registry ||= ConverterRegistry.new
|
10
10
|
end
|
11
11
|
|
12
|
-
def converter(
|
13
|
-
|
14
|
-
self
|
12
|
+
def converter(*names, &block)
|
13
|
+
converter_registry.register(*names, &block)
|
15
14
|
end
|
16
15
|
|
17
|
-
def convert(name,
|
18
|
-
|
16
|
+
def convert(name, input)
|
17
|
+
converter_registry[name].call(input)
|
19
18
|
end
|
20
19
|
end # ConverterDSL
|
21
20
|
end # Prompt
|
@@ -1,60 +1,65 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "forwardable"
|
4
|
+
|
3
5
|
module TTY
|
4
6
|
class Prompt
|
5
7
|
# Immutable collection of converters for type transformation
|
6
8
|
#
|
7
9
|
# @api private
|
8
10
|
class ConverterRegistry
|
11
|
+
extend Forwardable
|
12
|
+
|
13
|
+
def_delegators "@__registry", :keys
|
14
|
+
|
9
15
|
# Create a registry of conversions
|
10
16
|
#
|
11
17
|
# @param [Hash] registry
|
12
18
|
#
|
13
19
|
# @api private
|
14
20
|
def initialize(registry = {})
|
15
|
-
@
|
16
|
-
freeze
|
21
|
+
@__registry = registry.dup
|
17
22
|
end
|
18
23
|
|
19
|
-
#
|
24
|
+
# Check if conversion is available
|
20
25
|
#
|
21
|
-
# @param [
|
22
|
-
#
|
26
|
+
# @param [String] name
|
27
|
+
#
|
28
|
+
# @return [Boolean]
|
23
29
|
#
|
24
30
|
# @api public
|
25
|
-
def
|
26
|
-
|
27
|
-
|
28
|
-
if key?(name)
|
29
|
-
raise ArgumentError,
|
30
|
-
"Converter for #{name.inspect} already registered"
|
31
|
-
end
|
32
|
-
self.class.new(@_registry.merge(name => item))
|
31
|
+
def contain?(name)
|
32
|
+
conv_name = name.to_s.downcase.to_sym
|
33
|
+
@__registry.key?(conv_name)
|
33
34
|
end
|
34
35
|
|
35
|
-
#
|
36
|
+
# Register a conversion
|
36
37
|
#
|
37
|
-
# @
|
38
|
+
# @param [Symbol] name
|
39
|
+
# the converter name
|
38
40
|
#
|
39
41
|
# @api public
|
40
|
-
def
|
41
|
-
|
42
|
+
def register(*names, &block)
|
43
|
+
names.each do |name|
|
44
|
+
if contain?(name)
|
45
|
+
raise ConversionAlreadyDefined,
|
46
|
+
"converter for #{name.inspect} is already registered"
|
47
|
+
end
|
48
|
+
@__registry[name] = block
|
49
|
+
end
|
42
50
|
end
|
43
51
|
|
44
52
|
# Execute converter
|
45
53
|
#
|
46
54
|
# @api public
|
47
|
-
def
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
raise ArgumentError, "#{name.inspect} is not registered"
|
53
|
-
end
|
55
|
+
def [](name)
|
56
|
+
conv_name = name.to_s.downcase.to_sym
|
57
|
+
@__registry.fetch(conv_name) do
|
58
|
+
raise UnsupportedConversion,
|
59
|
+
"converter #{conv_name.inspect} is not registered"
|
54
60
|
end
|
55
|
-
converter[input]
|
56
61
|
end
|
57
|
-
alias []
|
62
|
+
alias fetch []
|
58
63
|
|
59
64
|
def inspect
|
60
65
|
@_registry.inspect
|
@@ -1,74 +1,181 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
require_relative 'converter_dsl'
|
3
|
+
require_relative "const"
|
4
|
+
require_relative "converter_dsl"
|
7
5
|
|
8
6
|
module TTY
|
9
7
|
class Prompt
|
10
8
|
module Converters
|
11
9
|
extend ConverterDSL
|
12
10
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
11
|
+
TRUE_VALUES = /^(t(rue)?|y(es)?|on|1)$/i.freeze
|
12
|
+
FALSE_VALUES = /^(f(alse)?|n(o)?|off|0)$/i.freeze
|
13
|
+
|
14
|
+
SINGLE_DIGIT_MATCHER = /^(?<digit>\-?\d+(\.\d+)?)$/.freeze
|
15
|
+
DIGIT_MATCHER = /^(?<open>-?\d+(\.\d+)?)
|
16
|
+
\s*(?<sep>(\.\s*){2,3}|-|,)\s*
|
17
|
+
(?<close>-?\d+(\.\d+)?)$
|
18
|
+
/x.freeze
|
19
|
+
LETTER_MATCHER = /^(?<open>\w)
|
20
|
+
\s*(?<sep>(\.\s*){2,3}|-|,)\s*
|
21
|
+
(?<close>\w)$
|
22
|
+
/x.freeze
|
23
|
+
|
24
|
+
converter(:boolean, :bool) do |input|
|
25
|
+
case input.to_s
|
26
|
+
when TRUE_VALUES then true
|
27
|
+
when FALSE_VALUES then false
|
28
|
+
else Const::Undefined
|
21
29
|
end
|
22
|
-
rescue Necromancer::ConversionTypeError => e
|
23
|
-
raise ConversionError, e.message
|
24
|
-
end
|
25
|
-
|
26
|
-
converter(:bool) do |input|
|
27
|
-
on_error { Necromancer.convert(input).to(:boolean, strict: true) }
|
28
30
|
end
|
29
31
|
|
30
|
-
converter(:string) do |input|
|
32
|
+
converter(:string, :str) do |input|
|
31
33
|
String(input).chomp
|
32
34
|
end
|
33
35
|
|
34
|
-
converter(:symbol) do |input|
|
36
|
+
converter(:symbol, :sym) do |input|
|
35
37
|
input.to_sym
|
36
38
|
end
|
37
39
|
|
40
|
+
converter(:char) do |input|
|
41
|
+
String(input).chars.to_a[0]
|
42
|
+
end
|
43
|
+
|
38
44
|
converter(:date) do |input|
|
39
|
-
|
45
|
+
begin
|
46
|
+
require "date" unless defined?(::Date)
|
47
|
+
::Date.parse(input)
|
48
|
+
rescue ArgumentError
|
49
|
+
Const::Undefined
|
50
|
+
end
|
40
51
|
end
|
41
52
|
|
42
53
|
converter(:datetime) do |input|
|
43
|
-
|
54
|
+
begin
|
55
|
+
require "date" unless defined?(::Date)
|
56
|
+
::DateTime.parse(input.to_s)
|
57
|
+
rescue ArgumentError
|
58
|
+
Const::Undefined
|
59
|
+
end
|
44
60
|
end
|
45
61
|
|
46
|
-
converter(:
|
47
|
-
|
62
|
+
converter(:time) do |input|
|
63
|
+
begin
|
64
|
+
require "time"
|
65
|
+
::Time.parse(input.to_s)
|
66
|
+
rescue ArgumentError
|
67
|
+
Const::Undefined
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
converter(:integer, :int) do |input|
|
72
|
+
begin
|
73
|
+
Integer(input)
|
74
|
+
rescue ArgumentError
|
75
|
+
Const::Undefined
|
76
|
+
end
|
48
77
|
end
|
49
78
|
|
50
79
|
converter(:float) do |input|
|
51
|
-
|
80
|
+
begin
|
81
|
+
Float(input)
|
82
|
+
rescue TypeError, ArgumentError
|
83
|
+
Const::Undefined
|
84
|
+
end
|
52
85
|
end
|
53
86
|
|
87
|
+
# Convert string number to integer or float
|
88
|
+
#
|
89
|
+
# @return [Integer,Float,Const::Undefined]
|
90
|
+
#
|
91
|
+
# @api private
|
92
|
+
def cast_to_num(num)
|
93
|
+
([convert(:int, num), convert(:float, num)] - [Const::Undefined]).first ||
|
94
|
+
Const::Undefined
|
95
|
+
end
|
96
|
+
module_function :cast_to_num
|
97
|
+
|
54
98
|
converter(:range) do |input|
|
55
|
-
|
99
|
+
if input.is_a?(::Range)
|
100
|
+
input
|
101
|
+
elsif match = input.to_s.match(SINGLE_DIGIT_MATCHER)
|
102
|
+
digit = cast_to_num(match[:digit])
|
103
|
+
::Range.new(digit, digit)
|
104
|
+
elsif match = input.to_s.match(DIGIT_MATCHER)
|
105
|
+
open = cast_to_num(match[:open])
|
106
|
+
close = cast_to_num(match[:close])
|
107
|
+
::Range.new(open, close, match[:sep].gsub(/\s*/, "") == "...")
|
108
|
+
elsif match = input.to_s.match(LETTER_MATCHER)
|
109
|
+
::Range.new(match[:open], match[:close],
|
110
|
+
match[:sep].gsub(/\s*/, "") == "...")
|
111
|
+
else Const::Undefined
|
112
|
+
end
|
56
113
|
end
|
57
114
|
|
58
115
|
converter(:regexp) do |input|
|
59
116
|
Regexp.new(input)
|
60
117
|
end
|
61
118
|
|
62
|
-
converter(:file) do |input|
|
63
|
-
::File.
|
119
|
+
converter(:filepath, :file) do |input|
|
120
|
+
::File.expand_path(input)
|
64
121
|
end
|
65
122
|
|
66
|
-
converter(:path) do |input|
|
67
|
-
|
123
|
+
converter(:pathname, :path) do |input|
|
124
|
+
require "pathname" unless defined?(::Pathname)
|
125
|
+
::Pathname.new(input)
|
68
126
|
end
|
69
127
|
|
70
|
-
converter(:
|
71
|
-
|
128
|
+
converter(:uri) do |input|
|
129
|
+
require "uri" unless defined?(::URI)
|
130
|
+
::URI.parse(input)
|
131
|
+
end
|
132
|
+
|
133
|
+
converter(:list, :array) do |val|
|
134
|
+
(val.respond_to?(:to_a) ? val : val.split(/(?<!\\),/))
|
135
|
+
.map { |v| v.strip.gsub(/\\,/, ",") }
|
136
|
+
.reject(&:empty?)
|
137
|
+
end
|
138
|
+
|
139
|
+
converter(:hash, :map) do |val|
|
140
|
+
values = val.respond_to?(:to_a) ? val : val.split(/[& ]/)
|
141
|
+
values.each_with_object({}) do |pair, pairs|
|
142
|
+
key, value = pair.split(/[=:]/, 2)
|
143
|
+
if (current = pairs[key.to_sym])
|
144
|
+
pairs[key.to_sym] = Array(current) << value
|
145
|
+
else
|
146
|
+
pairs[key.to_sym] = value
|
147
|
+
end
|
148
|
+
pairs
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
converter_registry.keys.each do |type|
|
153
|
+
next if type =~ /list|array|map|hash/
|
154
|
+
|
155
|
+
[:"#{type}_list", :"#{type}_array", :"#{type}s"].each do |new_type|
|
156
|
+
converter(new_type) do |val|
|
157
|
+
converter_registry[:array].(val).map do |obj|
|
158
|
+
converter_registry[type].(obj)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
[:"#{type}_map", :"#{type}_hash"].each do |new_type|
|
164
|
+
converter(new_type) do |val|
|
165
|
+
converter_registry[:hash].(val).each_with_object({}) do |(k, v), h|
|
166
|
+
h[k] = converter_registry[type].(v)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
[:"string_#{type}_map", :"str_#{type}_map",
|
172
|
+
:"string_#{type}_hash", :"str_#{type}_hash"].each do |new_type|
|
173
|
+
converter(new_type) do |val|
|
174
|
+
converter_registry[:hash].(val).each_with_object({}) do |(k, v), h|
|
175
|
+
h[converter_registry[:string].(k)] = converter_registry[type].(v)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
72
179
|
end
|
73
180
|
end # Converters
|
74
181
|
end # Prompt
|