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.
Files changed (80) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +25 -0
  5. data/CHANGELOG.md +218 -0
  6. data/CODE_OF_CONDUCT.md +49 -0
  7. data/Gemfile +19 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +1132 -0
  10. data/Rakefile +8 -0
  11. data/appveyor.yml +23 -0
  12. data/benchmarks/speed.rb +27 -0
  13. data/examples/ask.rb +15 -0
  14. data/examples/collect.rb +19 -0
  15. data/examples/echo.rb +11 -0
  16. data/examples/enum.rb +8 -0
  17. data/examples/enum_paged.rb +9 -0
  18. data/examples/enum_select.rb +7 -0
  19. data/examples/expand.rb +29 -0
  20. data/examples/in.rb +9 -0
  21. data/examples/inputs.rb +10 -0
  22. data/examples/key_events.rb +11 -0
  23. data/examples/keypress.rb +9 -0
  24. data/examples/mask.rb +13 -0
  25. data/examples/multi_select.rb +8 -0
  26. data/examples/multi_select_paged.rb +9 -0
  27. data/examples/multiline.rb +9 -0
  28. data/examples/pause.rb +7 -0
  29. data/examples/select.rb +18 -0
  30. data/examples/select_paginated.rb +9 -0
  31. data/examples/slider.rb +6 -0
  32. data/examples/validation.rb +9 -0
  33. data/examples/yes_no.rb +7 -0
  34. data/lib/tty-prompt.rb +4 -0
  35. data/lib/tty/prompt.rb +535 -0
  36. data/lib/tty/prompt/answers_collector.rb +59 -0
  37. data/lib/tty/prompt/choice.rb +90 -0
  38. data/lib/tty/prompt/choices.rb +110 -0
  39. data/lib/tty/prompt/confirm_question.rb +129 -0
  40. data/lib/tty/prompt/converter_dsl.rb +22 -0
  41. data/lib/tty/prompt/converter_registry.rb +64 -0
  42. data/lib/tty/prompt/converters.rb +77 -0
  43. data/lib/tty/prompt/distance.rb +49 -0
  44. data/lib/tty/prompt/enum_list.rb +337 -0
  45. data/lib/tty/prompt/enum_paginator.rb +56 -0
  46. data/lib/tty/prompt/evaluator.rb +29 -0
  47. data/lib/tty/prompt/expander.rb +292 -0
  48. data/lib/tty/prompt/keypress.rb +94 -0
  49. data/lib/tty/prompt/list.rb +317 -0
  50. data/lib/tty/prompt/mask_question.rb +91 -0
  51. data/lib/tty/prompt/multi_list.rb +108 -0
  52. data/lib/tty/prompt/multiline.rb +71 -0
  53. data/lib/tty/prompt/paginator.rb +88 -0
  54. data/lib/tty/prompt/question.rb +333 -0
  55. data/lib/tty/prompt/question/checks.rb +87 -0
  56. data/lib/tty/prompt/question/modifier.rb +94 -0
  57. data/lib/tty/prompt/question/validation.rb +72 -0
  58. data/lib/tty/prompt/reader.rb +352 -0
  59. data/lib/tty/prompt/reader/codes.rb +121 -0
  60. data/lib/tty/prompt/reader/console.rb +57 -0
  61. data/lib/tty/prompt/reader/history.rb +145 -0
  62. data/lib/tty/prompt/reader/key_event.rb +91 -0
  63. data/lib/tty/prompt/reader/line.rb +162 -0
  64. data/lib/tty/prompt/reader/mode.rb +44 -0
  65. data/lib/tty/prompt/reader/win_api.rb +29 -0
  66. data/lib/tty/prompt/reader/win_console.rb +53 -0
  67. data/lib/tty/prompt/result.rb +42 -0
  68. data/lib/tty/prompt/slider.rb +182 -0
  69. data/lib/tty/prompt/statement.rb +55 -0
  70. data/lib/tty/prompt/suggestion.rb +115 -0
  71. data/lib/tty/prompt/symbols.rb +61 -0
  72. data/lib/tty/prompt/timeout.rb +69 -0
  73. data/lib/tty/prompt/utils.rb +44 -0
  74. data/lib/tty/prompt/version.rb +7 -0
  75. data/lib/tty/test_prompt.rb +20 -0
  76. data/tasks/console.rake +11 -0
  77. data/tasks/coverage.rake +11 -0
  78. data/tasks/spec.rake +29 -0
  79. data/tty-prompt.gemspec +32 -0
  80. 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