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,44 @@
1
+ # encoding: utf-8
2
+
3
+ require 'io/console'
4
+
5
+ module TTY
6
+ class Prompt
7
+ class Reader
8
+ class Mode
9
+ # Initialize a Terminal
10
+ #
11
+ # @api public
12
+ def initialize(input = $stdin)
13
+ @input = input
14
+ end
15
+
16
+ # Echo given block
17
+ #
18
+ # @param [Boolean] is_on
19
+ #
20
+ # @api public
21
+ def echo(is_on = true, &block)
22
+ if is_on || !@input.tty?
23
+ yield
24
+ else
25
+ @input.noecho(&block)
26
+ end
27
+ end
28
+
29
+ # Use raw mode in the given block
30
+ #
31
+ # @param [Boolean] is_on
32
+ #
33
+ # @api public
34
+ def raw(is_on = true, &block)
35
+ if is_on && @input.tty?
36
+ @input.raw(&block)
37
+ else
38
+ yield
39
+ end
40
+ end
41
+ end # Mode
42
+ end # Reader
43
+ end # Prompt
44
+ end # TTY
@@ -0,0 +1,29 @@
1
+ # encoding: utf-8
2
+
3
+ require 'fiddle'
4
+
5
+ module TTY
6
+ class Prompt
7
+ class Reader
8
+ module WinAPI
9
+ include Fiddle
10
+
11
+ Handle = RUBY_VERSION >= "2.0.0" ? Fiddle::Handle : DL::Handle
12
+
13
+ CRT_HANDLE = Handle.new("msvcrt") rescue Handle.new("crtdll")
14
+
15
+ def getch
16
+ @@getch ||= Fiddle::Function.new(CRT_HANDLE["_getch"], [], TYPE_INT)
17
+ @@getch.call
18
+ end
19
+ module_function :getch
20
+
21
+ def getche
22
+ @@getche ||= Fiddle::Function.new(CRT_HANDLE["_getche"], [], TYPE_INT)
23
+ @@getche.call
24
+ end
25
+ module_function :getche
26
+ end # WinAPI
27
+ end # Reader
28
+ end # Prompt
29
+ end # TTY
@@ -0,0 +1,53 @@
1
+ # encoding: utf-8
2
+
3
+ require_relative 'codes'
4
+
5
+ module TTY
6
+ class Prompt
7
+ class Reader
8
+ class WinConsole
9
+ ESC = "\e".freeze
10
+ NUL_HEX = "\x00".freeze
11
+ EXT_HEX = "\xE0".freeze
12
+
13
+ # Key codes
14
+ #
15
+ # @return [Hash[Symbol]]
16
+ #
17
+ # @api public
18
+ attr_reader :keys
19
+
20
+ # Escape codes
21
+ #
22
+ # @return [Array[Integer]]
23
+ #
24
+ # @api public
25
+ attr_reader :escape_codes
26
+
27
+ def initialize(input)
28
+ require_relative 'win_api'
29
+ @input = input
30
+ @keys = Codes.win_keys
31
+ @escape_codes = [[NUL_HEX.ord], [ESC.ord], EXT_HEX.bytes.to_a]
32
+ end
33
+
34
+ # Get a character from console with echo
35
+ #
36
+ # @param [Hash[Symbol]] options
37
+ # @option options [Symbol] :echo
38
+ # the echo toggle
39
+ #
40
+ # @return [String]
41
+ #
42
+ # @api private
43
+ def get_char(options)
44
+ if options[:raw]
45
+ WinAPI.getch.chr
46
+ else
47
+ options[:echo] ? @input.getc : WinAPI.getch.chr
48
+ end
49
+ end
50
+ end # Console
51
+ end # Reader
52
+ end # Prompt
53
+ end # TTY
@@ -0,0 +1,42 @@
1
+ # encoding: utf-8
2
+
3
+ module TTY
4
+ class Prompt
5
+ # Accumulates errors
6
+ class Result
7
+ attr_reader :question, :value, :errors
8
+
9
+ def initialize(question, value, errors = [])
10
+ @question = question
11
+ @value = value
12
+ @errors = errors
13
+ end
14
+
15
+ def with(condition = nil, &block)
16
+ validator = (condition || block)
17
+ (new_value, validation_error) = validator.call(question, value)
18
+ accumulated_errors = errors + Array(validation_error)
19
+
20
+ if accumulated_errors.empty?
21
+ Success.new(question, new_value)
22
+ else
23
+ Failure.new(question, new_value, accumulated_errors)
24
+ end
25
+ end
26
+
27
+ def success?
28
+ is_a?(Success)
29
+ end
30
+
31
+ def failure?
32
+ is_a?(Failure)
33
+ end
34
+
35
+ class Success < Result
36
+ end
37
+
38
+ class Failure < Result
39
+ end
40
+ end
41
+ end # Prompt
42
+ end # TTY
@@ -0,0 +1,182 @@
1
+ # encoding: utf-8
2
+
3
+ require_relative 'symbols'
4
+
5
+ module TTY
6
+ # A class responsible for shell prompt interactions.
7
+ class Prompt
8
+ # A class responsible for gathering numeric input from range
9
+ #
10
+ # @api public
11
+ class Slider
12
+ include Symbols
13
+
14
+ HELP = '(Use arrow keys, press Enter to select)'.freeze
15
+
16
+ # Initailize a Slider
17
+ #
18
+ # @api public
19
+ def initialize(prompt, options = {})
20
+ @prompt = prompt
21
+ @prefix = options.fetch(:prefix) { @prompt.prefix }
22
+ @min = options.fetch(:min) { 0 }
23
+ @max = options.fetch(:max) { 10 }
24
+ @step = options.fetch(:step) { 1 }
25
+ @default = options[:default]
26
+ @active_color = options.fetch(:active_color) { @prompt.active_color }
27
+ @help_color = options.fetch(:help_color) { @prompt.help_color }
28
+ @first_render = true
29
+ @done = false
30
+
31
+ @prompt.subscribe(self)
32
+ end
33
+
34
+ # Setup initial active position
35
+ #
36
+ # @return [Integer]
37
+ #
38
+ # @api private
39
+ def initial
40
+ if @default.nil?
41
+ range.size / 2
42
+ else
43
+ range.index(@default)
44
+ end
45
+ end
46
+
47
+ # Range of numbers to render
48
+ #
49
+ # @return [Array[Integer]]
50
+ #
51
+ # @apip private
52
+ def range
53
+ (@min..@max).step(@step).to_a
54
+ end
55
+
56
+ # @api public
57
+ def default(value)
58
+ @default = value
59
+ end
60
+
61
+ # @api public
62
+ def min(value)
63
+ @min = value
64
+ end
65
+
66
+ # @api public
67
+ def max(value)
68
+ @max = value
69
+ end
70
+
71
+ # @api public
72
+ def step(value)
73
+ @step = value
74
+ end
75
+
76
+ # Call the slider by passing question
77
+ #
78
+ # @param [String] question
79
+ # the question to ask
80
+ #
81
+ # @apu public
82
+ def call(question, &block)
83
+ @question = question
84
+ block.call(self) if block
85
+ @active = initial
86
+ render
87
+ end
88
+
89
+ def keyleft(*)
90
+ @active -= 1 if @active > 0
91
+ end
92
+ alias_method :keydown, :keyleft
93
+
94
+ def keyright(*)
95
+ @active += 1 if (@active + @step) < range.size
96
+ end
97
+ alias_method :keyup, :keyright
98
+
99
+ def keyreturn(*)
100
+ @done = true
101
+ end
102
+ alias_method :keyspace, :keyreturn
103
+
104
+ private
105
+
106
+ # Render an interactive range slider.
107
+ #
108
+ # @api private
109
+ def render
110
+ @prompt.print(@prompt.hide)
111
+ until @done
112
+ question = render_question
113
+ @prompt.print(question)
114
+ @prompt.read_keypress
115
+ refresh(question.lines.count)
116
+ end
117
+ @prompt.print(render_question)
118
+ answer
119
+ ensure
120
+ @prompt.print(@prompt.show)
121
+ end
122
+
123
+ # Clear screen
124
+ #
125
+ # @param [Integer] lines
126
+ # the lines to clear
127
+ #
128
+ # @api private
129
+ def refresh(lines)
130
+ @prompt.print(@prompt.clear_lines(lines))
131
+ end
132
+
133
+ # @return [Integer]
134
+ #
135
+ # @api private
136
+ def answer
137
+ range[@active]
138
+ end
139
+
140
+ # Render question with the slider
141
+ #
142
+ # @return [String]
143
+ #
144
+ # @api private
145
+ def render_question
146
+ header = "#{@prefix}#{@question} #{render_header}\n"
147
+ @first_render = false
148
+ header << render_slider unless @done
149
+ header
150
+ end
151
+
152
+ # Render actual answer or help
153
+ #
154
+ # @return [String]
155
+ #
156
+ # @api private
157
+ def render_header
158
+ if @done
159
+ @prompt.decorate(answer.to_s, @active_color)
160
+ elsif @first_render
161
+ @prompt.decorate(HELP, @help_color)
162
+ end
163
+ end
164
+
165
+ # Render slider representation
166
+ #
167
+ # @return [String]
168
+ #
169
+ # @api private
170
+ def render_slider
171
+ output = ''
172
+ output << symbols[:pipe]
173
+ output << symbols[:line] * @active
174
+ output << @prompt.decorate(symbols[:handle], @active_color)
175
+ output << symbols[:line] * (range.size - @active - 1)
176
+ output << symbols[:pipe]
177
+ output << " #{range[@active]}"
178
+ output
179
+ end
180
+ end # Slider
181
+ end # Prompt
182
+ end # TTY
@@ -0,0 +1,55 @@
1
+ # encoding: utf-8
2
+
3
+ module TTY
4
+ # A class responsible for shell prompt interactions.
5
+ class Prompt
6
+ # A class representing a statement output to prompt.
7
+ class Statement
8
+ # Flag to display newline
9
+ #
10
+ # @api public
11
+ attr_reader :newline
12
+
13
+ # Color used to display statement
14
+ #
15
+ # @api public
16
+ attr_reader :color
17
+
18
+ # Initialize a Statement
19
+ #
20
+ # @param [TTY::Prompt] prompt
21
+ #
22
+ # @param [Hash] options
23
+ #
24
+ # @option options [Symbol] :newline
25
+ # force a newline break after the message
26
+ #
27
+ # @option options [Symbol] :color
28
+ # change the message display to color
29
+ #
30
+ # @api public
31
+ def initialize(prompt, options = {})
32
+ @prompt = prompt
33
+ @newline = options.fetch(:newline) { true }
34
+ @color = options.fetch(:color) { false }
35
+ end
36
+
37
+ # Output the message to the prompt
38
+ #
39
+ # @param [String] message
40
+ # the message to be printed to stdout
41
+ #
42
+ # @api public
43
+ def call(message)
44
+ message = @prompt.decorate(message, *color) if color
45
+
46
+ if newline && /( |\t)(\e\[\d+(;\d+)*m)?\Z/ !~ message
47
+ @prompt.puts message
48
+ else
49
+ @prompt.print message
50
+ @prompt.flush
51
+ end
52
+ end
53
+ end # Statement
54
+ end # Prompt
55
+ end # TTY
@@ -0,0 +1,115 @@
1
+ # encoding: utf-8
2
+
3
+ require_relative 'distance'
4
+
5
+ module TTY
6
+ # A class responsible for terminal prompt interactions.
7
+ class Prompt
8
+ # A class representing a suggestion out of possible choices
9
+ #
10
+ # @api public
11
+ class Suggestion
12
+ DEFAULT_INDENT = 8
13
+
14
+ SINGLE_TEXT = 'Did you mean this?'
15
+
16
+ PLURAL_TEXT = 'Did you mean one of these?'
17
+
18
+ # Number of spaces
19
+ #
20
+ # @api public
21
+ attr_reader :indent
22
+
23
+ # Text for a single suggestion
24
+ #
25
+ # @api public
26
+ attr_reader :single_text
27
+
28
+ # Text for multiple suggestions
29
+ #
30
+ # @api public
31
+ attr_reader :plural_text
32
+
33
+ # Initialize a Suggestion
34
+ #
35
+ # @api public
36
+ def initialize(options = {})
37
+ @indent = options.fetch(:indent) { DEFAULT_INDENT }
38
+ @single_text = options.fetch(:single_text) { SINGLE_TEXT }
39
+ @plural_text = options.fetch(:plural_text) { PLURAL_TEXT }
40
+ @suggestions = []
41
+ @comparator = Distance.new
42
+ end
43
+
44
+ # Suggest matches out of possibile strings
45
+ #
46
+ # @param [String] message
47
+ #
48
+ # @param [Array[String]] possibilities
49
+ #
50
+ # @api public
51
+ def suggest(message, possibilities)
52
+ distances = measure_distances(message, possibilities)
53
+ minimum_distance = distances.keys.min
54
+ max_distance = distances.keys.max
55
+
56
+ if minimum_distance < max_distance
57
+ @suggestions = distances[minimum_distance].sort
58
+ end
59
+ evaluate
60
+ end
61
+
62
+ private
63
+
64
+ # Measure distances between messag and possibilities
65
+ #
66
+ # @param [String] message
67
+ #
68
+ # @param [Array[String]] possibilities
69
+ #
70
+ # @return [Hash]
71
+ #
72
+ # @api private
73
+ def measure_distances(message, possibilities)
74
+ distances = Hash.new { |hash, key| hash[key] = [] }
75
+
76
+ possibilities.each do |possibility|
77
+ distances[@comparator.distance(message, possibility)] << possibility
78
+ end
79
+ distances
80
+ end
81
+
82
+ # Build up a suggestion string
83
+ #
84
+ # @param [Array[String]] suggestions
85
+ #
86
+ # @return [String]
87
+ #
88
+ # @api private
89
+ def evaluate
90
+ return @suggestions if @suggestions.empty?
91
+ if @suggestions.one?
92
+ build_single_suggestion
93
+ else
94
+ build_multiple_suggestions
95
+ end
96
+ end
97
+
98
+ # @api private
99
+ def build_single_suggestion
100
+ suggestion = ''
101
+ suggestion << single_text + "\n"
102
+ suggestion << (' ' * indent + @suggestions.first)
103
+ end
104
+
105
+ # @api private
106
+ def build_multiple_suggestions
107
+ suggestion = ''
108
+ suggestion << plural_text + "\n"
109
+ suggestion << @suggestions.map do |sugest|
110
+ ' ' * indent + sugest
111
+ end.join("\n")
112
+ end
113
+ end # Suggestion
114
+ end # Prompt
115
+ end # TTY