austb-tty-prompt 0.13.0

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