austb-tty-prompt 0.13.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 +7 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.travis.yml +25 -0
- data/CHANGELOG.md +218 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +19 -0
- data/LICENSE.txt +22 -0
- data/README.md +1132 -0
- data/Rakefile +8 -0
- data/appveyor.yml +23 -0
- data/benchmarks/speed.rb +27 -0
- data/examples/ask.rb +15 -0
- data/examples/collect.rb +19 -0
- data/examples/echo.rb +11 -0
- data/examples/enum.rb +8 -0
- data/examples/enum_paged.rb +9 -0
- data/examples/enum_select.rb +7 -0
- data/examples/expand.rb +29 -0
- data/examples/in.rb +9 -0
- data/examples/inputs.rb +10 -0
- data/examples/key_events.rb +11 -0
- data/examples/keypress.rb +9 -0
- data/examples/mask.rb +13 -0
- data/examples/multi_select.rb +8 -0
- data/examples/multi_select_paged.rb +9 -0
- data/examples/multiline.rb +9 -0
- data/examples/pause.rb +7 -0
- data/examples/select.rb +18 -0
- data/examples/select_paginated.rb +9 -0
- data/examples/slider.rb +6 -0
- data/examples/validation.rb +9 -0
- data/examples/yes_no.rb +7 -0
- data/lib/tty-prompt.rb +4 -0
- data/lib/tty/prompt.rb +535 -0
- data/lib/tty/prompt/answers_collector.rb +59 -0
- data/lib/tty/prompt/choice.rb +90 -0
- data/lib/tty/prompt/choices.rb +110 -0
- data/lib/tty/prompt/confirm_question.rb +129 -0
- data/lib/tty/prompt/converter_dsl.rb +22 -0
- data/lib/tty/prompt/converter_registry.rb +64 -0
- data/lib/tty/prompt/converters.rb +77 -0
- data/lib/tty/prompt/distance.rb +49 -0
- data/lib/tty/prompt/enum_list.rb +337 -0
- data/lib/tty/prompt/enum_paginator.rb +56 -0
- data/lib/tty/prompt/evaluator.rb +29 -0
- data/lib/tty/prompt/expander.rb +292 -0
- data/lib/tty/prompt/keypress.rb +94 -0
- data/lib/tty/prompt/list.rb +317 -0
- data/lib/tty/prompt/mask_question.rb +91 -0
- data/lib/tty/prompt/multi_list.rb +108 -0
- data/lib/tty/prompt/multiline.rb +71 -0
- data/lib/tty/prompt/paginator.rb +88 -0
- data/lib/tty/prompt/question.rb +333 -0
- data/lib/tty/prompt/question/checks.rb +87 -0
- data/lib/tty/prompt/question/modifier.rb +94 -0
- data/lib/tty/prompt/question/validation.rb +72 -0
- data/lib/tty/prompt/reader.rb +352 -0
- data/lib/tty/prompt/reader/codes.rb +121 -0
- data/lib/tty/prompt/reader/console.rb +57 -0
- data/lib/tty/prompt/reader/history.rb +145 -0
- data/lib/tty/prompt/reader/key_event.rb +91 -0
- data/lib/tty/prompt/reader/line.rb +162 -0
- data/lib/tty/prompt/reader/mode.rb +44 -0
- data/lib/tty/prompt/reader/win_api.rb +29 -0
- data/lib/tty/prompt/reader/win_console.rb +53 -0
- data/lib/tty/prompt/result.rb +42 -0
- data/lib/tty/prompt/slider.rb +182 -0
- data/lib/tty/prompt/statement.rb +55 -0
- data/lib/tty/prompt/suggestion.rb +115 -0
- data/lib/tty/prompt/symbols.rb +61 -0
- data/lib/tty/prompt/timeout.rb +69 -0
- data/lib/tty/prompt/utils.rb +44 -0
- data/lib/tty/prompt/version.rb +7 -0
- data/lib/tty/test_prompt.rb +20 -0
- data/tasks/console.rake +11 -0
- data/tasks/coverage.rake +11 -0
- data/tasks/spec.rake +29 -0
- data/tty-prompt.gemspec +32 -0
- metadata +243 -0
@@ -0,0 +1,59 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module TTY
|
4
|
+
class Prompt
|
5
|
+
class AnswersCollector
|
6
|
+
# Initialize answer collector
|
7
|
+
#
|
8
|
+
# @api public
|
9
|
+
def initialize(prompt, options = {})
|
10
|
+
@prompt = prompt
|
11
|
+
@answers = options.fetch(:answers) { {} }
|
12
|
+
end
|
13
|
+
|
14
|
+
# Start gathering answers
|
15
|
+
#
|
16
|
+
# @return [Hash]
|
17
|
+
# the collection of all answers
|
18
|
+
#
|
19
|
+
# @api public
|
20
|
+
def call(&block)
|
21
|
+
instance_eval(&block)
|
22
|
+
@answers
|
23
|
+
end
|
24
|
+
|
25
|
+
# Create answer entry
|
26
|
+
#
|
27
|
+
# @example
|
28
|
+
# key(:name).ask('Name?')
|
29
|
+
#
|
30
|
+
# @api public
|
31
|
+
def key(name, &block)
|
32
|
+
@name = name
|
33
|
+
if block
|
34
|
+
answer = create_collector.(&block)
|
35
|
+
add_answer(answer)
|
36
|
+
end
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
# @api public
|
41
|
+
def create_collector
|
42
|
+
self.class.new(@prompt)
|
43
|
+
end
|
44
|
+
|
45
|
+
# @api public
|
46
|
+
def add_answer(answer)
|
47
|
+
@answers[@name] = answer
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
# @api private
|
53
|
+
def method_missing(method, *args, &block)
|
54
|
+
answer = @prompt.public_send(method, *args, &block)
|
55
|
+
add_answer(answer)
|
56
|
+
end
|
57
|
+
end # AnswersCollector
|
58
|
+
end # Prompt
|
59
|
+
end # TTY
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module TTY
|
4
|
+
class Prompt
|
5
|
+
# A single choice option
|
6
|
+
#
|
7
|
+
# @api public
|
8
|
+
class Choice
|
9
|
+
# The label name
|
10
|
+
#
|
11
|
+
# @api public
|
12
|
+
attr_reader :name
|
13
|
+
|
14
|
+
attr_reader :key
|
15
|
+
|
16
|
+
# Create a Choice instance
|
17
|
+
#
|
18
|
+
# @api public
|
19
|
+
def initialize(name, value, key = nil)
|
20
|
+
@name = name
|
21
|
+
@value = value
|
22
|
+
@key = key
|
23
|
+
end
|
24
|
+
|
25
|
+
# Create choice from value
|
26
|
+
#
|
27
|
+
# @example
|
28
|
+
# Choice.from(:option_1)
|
29
|
+
# Choice.from([:option_1, 1])
|
30
|
+
#
|
31
|
+
# @param [Object] val
|
32
|
+
# the value to be converted
|
33
|
+
#
|
34
|
+
# @raise [ArgumentError]
|
35
|
+
#
|
36
|
+
# @return [Choice]
|
37
|
+
#
|
38
|
+
# @api public
|
39
|
+
def self.from(val)
|
40
|
+
case val
|
41
|
+
when Choice
|
42
|
+
val
|
43
|
+
when String, Symbol
|
44
|
+
new(val, val)
|
45
|
+
when Array
|
46
|
+
new("#{val.first}", val.last)
|
47
|
+
when Hash
|
48
|
+
if val.key?(:name)
|
49
|
+
new("#{val[:name]}", val[:value], val[:key])
|
50
|
+
else
|
51
|
+
new("#{val.keys.first}", val.values.first)
|
52
|
+
end
|
53
|
+
else
|
54
|
+
raise ArgumentError, "#{val} cannot be coerced into Choice"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Read value and evaluate
|
59
|
+
#
|
60
|
+
# @api public
|
61
|
+
def value
|
62
|
+
case @value
|
63
|
+
when Proc
|
64
|
+
@value.call
|
65
|
+
else
|
66
|
+
@value
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Object equality comparison
|
71
|
+
#
|
72
|
+
# @return [Boolean]
|
73
|
+
#
|
74
|
+
# @api public
|
75
|
+
def ==(other)
|
76
|
+
return false unless other.is_a?(self.class)
|
77
|
+
name == other.name && value == other.value
|
78
|
+
end
|
79
|
+
|
80
|
+
# Object string representation
|
81
|
+
#
|
82
|
+
# @return [String]
|
83
|
+
#
|
84
|
+
# @api public
|
85
|
+
def to_s
|
86
|
+
"#{name}"
|
87
|
+
end
|
88
|
+
end # Choice
|
89
|
+
end # Prompt
|
90
|
+
end # TTY
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
require_relative 'choice'
|
6
|
+
|
7
|
+
module TTY
|
8
|
+
class Prompt
|
9
|
+
# A class responsible for storing a collection of choices
|
10
|
+
#
|
11
|
+
# @api private
|
12
|
+
class Choices
|
13
|
+
include Enumerable
|
14
|
+
extend Forwardable
|
15
|
+
|
16
|
+
# The actual collection choices
|
17
|
+
#
|
18
|
+
# @return [Array[Choice]]
|
19
|
+
#
|
20
|
+
# @api public
|
21
|
+
attr_reader :choices
|
22
|
+
|
23
|
+
def_delegators :choices, :length, :size, :to_ary, :empty?, :values_at
|
24
|
+
|
25
|
+
# Convenience for creating choices
|
26
|
+
#
|
27
|
+
# @param [Array[Object]] choices
|
28
|
+
# the choice objects
|
29
|
+
#
|
30
|
+
# @return [Choices]
|
31
|
+
# the choices collection
|
32
|
+
#
|
33
|
+
# @api public
|
34
|
+
def self.[](*choices)
|
35
|
+
new(choices)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Create Choices collection
|
39
|
+
#
|
40
|
+
# @param [Array[Choice]] choices
|
41
|
+
# the choices to add to collection
|
42
|
+
#
|
43
|
+
# @api public
|
44
|
+
def initialize(choices = [])
|
45
|
+
@choices = choices.map do |choice|
|
46
|
+
Choice.from(choice)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Iterate over all choices in the collection
|
51
|
+
#
|
52
|
+
# @yield [Choice]
|
53
|
+
#
|
54
|
+
# @api public
|
55
|
+
def each(&block)
|
56
|
+
return to_enum unless block_given?
|
57
|
+
choices.each(&block)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Add choice to collection
|
61
|
+
#
|
62
|
+
# @param [Object] choice
|
63
|
+
# the choice to add
|
64
|
+
#
|
65
|
+
# @api public
|
66
|
+
def <<(choice)
|
67
|
+
choices << Choice.from(choice)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Access choice by index
|
71
|
+
#
|
72
|
+
# @param [Integer] index
|
73
|
+
#
|
74
|
+
# @return [Choice]
|
75
|
+
#
|
76
|
+
# @api public
|
77
|
+
def [](index)
|
78
|
+
@choices[index]
|
79
|
+
end
|
80
|
+
|
81
|
+
# Pluck a choice by its name from collection
|
82
|
+
#
|
83
|
+
# @param [String] name
|
84
|
+
# the label name for the choice
|
85
|
+
#
|
86
|
+
# @return [Choice]
|
87
|
+
#
|
88
|
+
# @api public
|
89
|
+
def pluck(name)
|
90
|
+
map { |choice| choice.public_send(name) }
|
91
|
+
end
|
92
|
+
|
93
|
+
# Find a matching choice
|
94
|
+
#
|
95
|
+
# @exmaple
|
96
|
+
# choices.find_by(:name, 'small')
|
97
|
+
#
|
98
|
+
# @param [Symbol] attr
|
99
|
+
# the attribute name
|
100
|
+
# @param [Object] value
|
101
|
+
#
|
102
|
+
# @return [Choice]
|
103
|
+
#
|
104
|
+
# @api public
|
105
|
+
def find_by(attr, value)
|
106
|
+
find { |choice| choice.public_send(attr) == value }
|
107
|
+
end
|
108
|
+
end # Choices
|
109
|
+
end # Prompt
|
110
|
+
end # TTY
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require_relative 'question'
|
4
|
+
require_relative 'utils'
|
5
|
+
|
6
|
+
module TTY
|
7
|
+
class Prompt
|
8
|
+
class ConfirmQuestion < Question
|
9
|
+
# Create confirmation question
|
10
|
+
#
|
11
|
+
# @param [Hash] options
|
12
|
+
# @option options [String] :suffix
|
13
|
+
# @option options [String] :positive
|
14
|
+
# @option options [String] :negative
|
15
|
+
#
|
16
|
+
# @api public
|
17
|
+
def initialize(prompt, options = {})
|
18
|
+
super
|
19
|
+
@suffix = options.fetch(:suffix) { UndefinedSetting }
|
20
|
+
@positive = options.fetch(:positive) { UndefinedSetting }
|
21
|
+
@negative = options.fetch(:negative) { UndefinedSetting }
|
22
|
+
end
|
23
|
+
|
24
|
+
def positive?
|
25
|
+
@positive != UndefinedSetting
|
26
|
+
end
|
27
|
+
|
28
|
+
def negative?
|
29
|
+
@negative != UndefinedSetting
|
30
|
+
end
|
31
|
+
|
32
|
+
def suffix?
|
33
|
+
@suffix != UndefinedSetting
|
34
|
+
end
|
35
|
+
|
36
|
+
# Set question suffix
|
37
|
+
#
|
38
|
+
# @api public
|
39
|
+
def suffix(value = (not_set = true))
|
40
|
+
return @negative if not_set
|
41
|
+
@suffix = value
|
42
|
+
end
|
43
|
+
|
44
|
+
# Set value for matching positive choice
|
45
|
+
#
|
46
|
+
# @api public
|
47
|
+
def positive(value = (not_set = true))
|
48
|
+
return @positive if not_set
|
49
|
+
@positive = value
|
50
|
+
end
|
51
|
+
|
52
|
+
# Set value for matching negative choice
|
53
|
+
#
|
54
|
+
# @api public
|
55
|
+
def negative(value = (not_set = true))
|
56
|
+
return @negative if not_set
|
57
|
+
@negative = value
|
58
|
+
end
|
59
|
+
|
60
|
+
def call(message, &block)
|
61
|
+
return if Utils.blank?(message)
|
62
|
+
@message = message
|
63
|
+
block.call(self) if block
|
64
|
+
setup_defaults
|
65
|
+
render
|
66
|
+
end
|
67
|
+
|
68
|
+
# Render confirmation question
|
69
|
+
#
|
70
|
+
# @return [String]
|
71
|
+
#
|
72
|
+
# @api private
|
73
|
+
def render_question
|
74
|
+
header = "#{@prefix}#{message} "
|
75
|
+
if !@done
|
76
|
+
header += @prompt.decorate("(#{@suffix})", @help_color) + ' '
|
77
|
+
else
|
78
|
+
answer = convert_result(@input)
|
79
|
+
label = answer ? @positive : @negative
|
80
|
+
header += @prompt.decorate(label, @active_color)
|
81
|
+
end
|
82
|
+
header << "\n" if @done
|
83
|
+
header
|
84
|
+
end
|
85
|
+
|
86
|
+
protected
|
87
|
+
|
88
|
+
# @api private
|
89
|
+
def setup_defaults
|
90
|
+
return if suffix? && positive?
|
91
|
+
|
92
|
+
if suffix? && (!positive? || !negative?)
|
93
|
+
parts = @suffix.split('/')
|
94
|
+
@positive = parts[0]
|
95
|
+
@negative = parts[1]
|
96
|
+
@convert = conversion
|
97
|
+
elsif !suffix? && positive?
|
98
|
+
@suffix = create_suffix
|
99
|
+
@convert = conversion
|
100
|
+
else
|
101
|
+
create_default_labels
|
102
|
+
@convert = :bool
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# @api private
|
107
|
+
def create_default_labels
|
108
|
+
@suffix = default ? 'Y/n' : 'y/N'
|
109
|
+
@positive = default ? 'Yes' : 'yes'
|
110
|
+
@negative = default ? 'no' : 'No'
|
111
|
+
end
|
112
|
+
|
113
|
+
# @api private
|
114
|
+
def create_suffix
|
115
|
+
result = ''
|
116
|
+
result << "#{default ? positive.capitalize : positive.downcase}"
|
117
|
+
result << '/'
|
118
|
+
result << "#{default ? negative.downcase : negative.capitalize}"
|
119
|
+
end
|
120
|
+
|
121
|
+
# Create custom conversion
|
122
|
+
#
|
123
|
+
# @api private
|
124
|
+
def conversion
|
125
|
+
proc { |input| !input.match(/^#{positive}|#{positive[0]}$/i).nil? }
|
126
|
+
end
|
127
|
+
end # ConfirmQuestion
|
128
|
+
end # Prompt
|
129
|
+
end # TTY
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require_relative 'converter_registry'
|
4
|
+
|
5
|
+
module TTY
|
6
|
+
class Prompt
|
7
|
+
module ConverterDSL
|
8
|
+
def converter_registry
|
9
|
+
@converter_registry ||= ConverterRegistry.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def converter(name, &block)
|
13
|
+
@converter_registry = converter_registry.register(name, &block)
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
def convert(name, data)
|
18
|
+
@converter_registry[name, data]
|
19
|
+
end
|
20
|
+
end # ConverterDSL
|
21
|
+
end # Prompt
|
22
|
+
end # TTY
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module TTY
|
4
|
+
class Prompt
|
5
|
+
# Immutable collection of converters for type transformation
|
6
|
+
#
|
7
|
+
# @api private
|
8
|
+
class ConverterRegistry
|
9
|
+
# Create a registry of conversions
|
10
|
+
#
|
11
|
+
# @param [Hash] registry
|
12
|
+
#
|
13
|
+
# @api private
|
14
|
+
def initialize(registry = {})
|
15
|
+
@_registry = registry.dup.freeze
|
16
|
+
freeze
|
17
|
+
end
|
18
|
+
|
19
|
+
# Register converter
|
20
|
+
#
|
21
|
+
# @param [Symbol] name
|
22
|
+
# the converter name
|
23
|
+
#
|
24
|
+
# @api public
|
25
|
+
def register(name, contents = nil, &block)
|
26
|
+
item = block_given? ? block : contents
|
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))
|
33
|
+
end
|
34
|
+
|
35
|
+
# Check if converter is registered
|
36
|
+
#
|
37
|
+
# @return [Boolean]
|
38
|
+
#
|
39
|
+
# @api public
|
40
|
+
def key?(key)
|
41
|
+
@_registry.key?(key)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Execute converter
|
45
|
+
#
|
46
|
+
# @api public
|
47
|
+
def call(name, input)
|
48
|
+
if name.respond_to?(:call)
|
49
|
+
converter = name
|
50
|
+
else
|
51
|
+
converter = @_registry.fetch(name) do
|
52
|
+
raise ArgumentError, "#{name.inspect} is not registered"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
converter[input]
|
56
|
+
end
|
57
|
+
alias [] call
|
58
|
+
|
59
|
+
def inspect
|
60
|
+
@_registry.inspect
|
61
|
+
end
|
62
|
+
end # ConverterRegistry
|
63
|
+
end # Prompt
|
64
|
+
end # TTY
|