austb-tty-prompt 0.13.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|