tty2-prompt 0.23.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +14 -0
  3. data/LICENSE.txt +23 -0
  4. data/README.md +52 -0
  5. data/lib/tty2/prompt/answers_collector.rb +78 -0
  6. data/lib/tty2/prompt/block_paginator.rb +59 -0
  7. data/lib/tty2/prompt/choice.rb +147 -0
  8. data/lib/tty2/prompt/choices.rb +129 -0
  9. data/lib/tty2/prompt/confirm_question.rb +158 -0
  10. data/lib/tty2/prompt/const.rb +17 -0
  11. data/lib/tty2/prompt/converter_dsl.rb +21 -0
  12. data/lib/tty2/prompt/converter_registry.rb +69 -0
  13. data/lib/tty2/prompt/converters.rb +182 -0
  14. data/lib/tty2/prompt/distance.rb +49 -0
  15. data/lib/tty2/prompt/enum_list.rb +433 -0
  16. data/lib/tty2/prompt/errors.rb +31 -0
  17. data/lib/tty2/prompt/evaluator.rb +29 -0
  18. data/lib/tty2/prompt/expander.rb +321 -0
  19. data/lib/tty2/prompt/keypress.rb +98 -0
  20. data/lib/tty2/prompt/list.rb +589 -0
  21. data/lib/tty2/prompt/mask_question.rb +96 -0
  22. data/lib/tty2/prompt/multi_list.rb +224 -0
  23. data/lib/tty2/prompt/multiline.rb +72 -0
  24. data/lib/tty2/prompt/paginator.rb +111 -0
  25. data/lib/tty2/prompt/question/checks.rb +105 -0
  26. data/lib/tty2/prompt/question/modifier.rb +96 -0
  27. data/lib/tty2/prompt/question/validation.rb +72 -0
  28. data/lib/tty2/prompt/question.rb +391 -0
  29. data/lib/tty2/prompt/result.rb +42 -0
  30. data/lib/tty2/prompt/selected_choices.rb +77 -0
  31. data/lib/tty2/prompt/slider.rb +286 -0
  32. data/lib/tty2/prompt/statement.rb +55 -0
  33. data/lib/tty2/prompt/suggestion.rb +113 -0
  34. data/lib/tty2/prompt/symbols.rb +89 -0
  35. data/lib/tty2/prompt/test.rb +36 -0
  36. data/lib/tty2/prompt/timer.rb +75 -0
  37. data/lib/tty2/prompt/utils.rb +42 -0
  38. data/lib/tty2/prompt/version.rb +7 -0
  39. data/lib/tty2/prompt.rb +589 -0
  40. data/lib/tty2-prompt.rb +1 -0
  41. metadata +148 -0
@@ -0,0 +1,286 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTY2
4
+ # A class responsible for shell prompt interactions.
5
+ class Prompt
6
+ # A class responsible for gathering numeric input from range
7
+ #
8
+ # @api public
9
+ class Slider
10
+ HELP = "(Use %s arrow keys, press Enter to select)"
11
+
12
+ FORMAT = ":slider %s"
13
+
14
+ # Initailize a Slider
15
+ #
16
+ # @param [Prompt] prompt
17
+ # the prompt
18
+ # @param [Hash] options
19
+ # the options to configure this slider
20
+ # @option options [Integer] :min The minimum value
21
+ # @option options [Integer] :max The maximum value
22
+ # @option options [Integer] :step The step value
23
+ # @option options [String] :format The display format
24
+ #
25
+ # @api public
26
+ def initialize(prompt, **options)
27
+ @prompt = prompt
28
+ @prefix = options.fetch(:prefix) { @prompt.prefix }
29
+ @choices = Choices.new
30
+ @min = options.fetch(:min, 0)
31
+ @max = options.fetch(:max, 10)
32
+ @step = options.fetch(:step, 1)
33
+ @default = options[:default]
34
+ @active_color = options.fetch(:active_color) { @prompt.active_color }
35
+ @help_color = options.fetch(:help_color) { @prompt.help_color }
36
+ @format = options.fetch(:format) { FORMAT }
37
+ @quiet = options.fetch(:quiet) { @prompt.quiet }
38
+ @help = options[:help]
39
+ @show_help = options.fetch(:show_help) { :start }
40
+ @symbols = @prompt.symbols.merge(options.fetch(:symbols, {}))
41
+ @first_render = true
42
+ @done = false
43
+ end
44
+
45
+ # Change symbols used by this prompt
46
+ #
47
+ # @param [Hash] new_symbols
48
+ # the new symbols to use
49
+ #
50
+ # @api public
51
+ def symbols(new_symbols = (not_set = true))
52
+ return @symbols if not_set
53
+
54
+ @symbols.merge!(new_symbols)
55
+ end
56
+
57
+ # Setup initial active position
58
+ #
59
+ # @return [Integer]
60
+ #
61
+ # @api private
62
+ def initial
63
+ if @default.nil?
64
+ # no default - choose the middle option
65
+ choices.size / 2
66
+ elsif default_choice = choices.find_by(:name, @default)
67
+ # found a Choice by name - use it
68
+ choices.index(default_choice)
69
+ else
70
+ # default is the index number
71
+ @default - 1
72
+ end
73
+ end
74
+
75
+ # Default help text
76
+ #
77
+ # @api public
78
+ def default_help
79
+ arrows = @symbols[:arrow_left] + "/" + @symbols[:arrow_right]
80
+ sprintf(HELP, arrows)
81
+ end
82
+
83
+ # Set help text
84
+ #
85
+ # @param [String] text
86
+ #
87
+ # @api private
88
+ def help(text = (not_set = true))
89
+ return @help if !@help.nil? && not_set
90
+
91
+ @help = (@help.nil? && not_set) ? default_help : text
92
+ end
93
+
94
+ # Change when help is displayed
95
+ #
96
+ # @api public
97
+ def show_help(value = (not_set = true))
98
+ return @show_ehlp if not_set
99
+
100
+ @show_help = value
101
+ end
102
+
103
+ # @api public
104
+ def default(value)
105
+ @default = value
106
+ end
107
+
108
+ # @api public
109
+ def min(value)
110
+ @min = value
111
+ end
112
+
113
+ # @api public
114
+ def max(value)
115
+ @max = value
116
+ end
117
+
118
+ # @api public
119
+ def step(value)
120
+ @step = value
121
+ end
122
+
123
+ # Add a single choice
124
+ #
125
+ # @api public
126
+ def choice(*value, &block)
127
+ if block
128
+ @choices << (value << block)
129
+ else
130
+ @choices << value
131
+ end
132
+ end
133
+
134
+ # Add multiple choices
135
+ #
136
+ # @param [Array[Object]] values
137
+ # the values to add as choices
138
+ #
139
+ # @api public
140
+ def choices(values = (not_set = true))
141
+ if not_set
142
+ @choices
143
+ else
144
+ values.each { |val| @choices << val }
145
+ end
146
+ end
147
+
148
+ # @api public
149
+ def format(value)
150
+ @format = value
151
+ end
152
+
153
+ # Set quiet mode.
154
+ #
155
+ # @api public
156
+ def quiet(value)
157
+ @quiet = value
158
+ end
159
+
160
+ # Call the slider by passing question
161
+ #
162
+ # @param [String] question
163
+ # the question to ask
164
+ #
165
+ # @apu public
166
+ def call(question, possibilities = nil, &block)
167
+ @question = question
168
+ choices(possibilities) if possibilities
169
+ block.call(self) if block
170
+ # set up a Choices collection for min, max, step
171
+ # if no possibilities were supplied
172
+ choices((@min..@max).step(@step).to_a) if @choices.empty?
173
+
174
+ @active = initial
175
+ @prompt.subscribe(self) do
176
+ render
177
+ end
178
+ end
179
+
180
+ def keyleft(*)
181
+ @active -= 1 if @active > 0
182
+ end
183
+ alias keydown keyleft
184
+
185
+ def keyright(*)
186
+ @active += 1 if (@active + 1) < choices.size
187
+ end
188
+ alias keyup keyright
189
+
190
+ def keyreturn(*)
191
+ @done = true
192
+ end
193
+ alias keyspace keyreturn
194
+ alias keyenter keyreturn
195
+
196
+ private
197
+
198
+ # Check if help is shown only on start
199
+ #
200
+ # @api private
201
+ def help_start?
202
+ @show_help =~ /start/i
203
+ end
204
+
205
+ # Check if help is always displayed
206
+ #
207
+ # @api private
208
+ def help_always?
209
+ @show_help =~ /always/i
210
+ end
211
+
212
+ # Render an interactive range slider.
213
+ #
214
+ # @api private
215
+ def render
216
+ @prompt.print(@prompt.hide)
217
+ until @done
218
+ question = render_question
219
+ @prompt.print(question)
220
+ @prompt.read_keypress
221
+ refresh(question.lines.count)
222
+ end
223
+ @prompt.print(render_question) unless @quiet
224
+ answer
225
+ ensure
226
+ @prompt.print(@prompt.show)
227
+ end
228
+
229
+ # Clear screen
230
+ #
231
+ # @param [Integer] lines
232
+ # the lines to clear
233
+ #
234
+ # @api private
235
+ def refresh(lines)
236
+ @prompt.print(@prompt.clear_lines(lines))
237
+ end
238
+
239
+ # @return [Integer, String]
240
+ #
241
+ # @api private
242
+ def answer
243
+ choices[@active].value
244
+ end
245
+
246
+ # Render question with the slider
247
+ #
248
+ # @return [String]
249
+ #
250
+ # @api private
251
+ def render_question
252
+ header = ["#{@prefix}#{@question} "]
253
+ if @done
254
+ header << @prompt.decorate(choices[@active].to_s, @active_color)
255
+ header << "\n"
256
+ else
257
+ header << render_slider
258
+ end
259
+ if @first_render && (help_start? || help_always?) ||
260
+ (help_always? && !@done)
261
+ header << "\n" + @prompt.decorate(help, @help_color)
262
+ @first_render = false
263
+ end
264
+ header.join
265
+ end
266
+
267
+ # Render slider representation
268
+ #
269
+ # @return [String]
270
+ #
271
+ # @api private
272
+ def render_slider
273
+ slider = (@symbols[:line] * @active) +
274
+ @prompt.decorate(@symbols[:bullet], @active_color) +
275
+ (@symbols[:line] * (choices.size - @active - 1))
276
+ value = choices[@active].name
277
+ case @format
278
+ when Proc
279
+ @format.call(slider, value)
280
+ else
281
+ @format.gsub(":slider", slider) % [value]
282
+ end
283
+ end
284
+ end # Slider
285
+ end # Prompt
286
+ end # TTY2
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTY2
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 [TTY2::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, newline: true, color: false)
32
+ @prompt = prompt
33
+ @newline = newline
34
+ @color = color
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 # TTY2
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "distance"
4
+
5
+ module TTY2
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
+
92
+ if @suggestions.one?
93
+ build_single_suggestion
94
+ else
95
+ build_multiple_suggestions
96
+ end
97
+ end
98
+
99
+ # @api private
100
+ def build_single_suggestion
101
+ single_text + "\n" + (" " * indent) + @suggestions.first
102
+ end
103
+
104
+ # @api private
105
+ def build_multiple_suggestions
106
+ plural_text + "\n" +
107
+ @suggestions.map do |sugest|
108
+ " " * indent + sugest
109
+ end.join("\n")
110
+ end
111
+ end # Suggestion
112
+ end # Prompt
113
+ end # TTY2
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTY2
4
+ class Prompt
5
+ # Cross platform common Unicode symbols.
6
+ #
7
+ # @api public
8
+ module Symbols
9
+ KEYS = {
10
+ tick: "✓",
11
+ cross: "✘",
12
+ star: "★",
13
+ square: "◼",
14
+ square_empty: "◻",
15
+ dot: "•",
16
+ bullet: "●",
17
+ bullet_empty: "○",
18
+ marker: "‣",
19
+ line: "─",
20
+ pipe: "|",
21
+ ellipsis: "…",
22
+ radio_on: "⬢",
23
+ radio_off: "⬡",
24
+ checkbox_on: "☒",
25
+ checkbox_off: "☐",
26
+ circle: "◯",
27
+ circle_on: "ⓧ",
28
+ circle_off: "Ⓘ",
29
+ arrow_up: "↑",
30
+ arrow_down: "↓",
31
+ arrow_up_down: "↕",
32
+ arrow_left: "←",
33
+ arrow_right: "→",
34
+ arrow_left_right: "↔",
35
+ heart: "♥",
36
+ diamond: "♦",
37
+ club: "♣",
38
+ spade: "♠"
39
+ }.freeze
40
+
41
+ WIN_KEYS = {
42
+ tick: "√",
43
+ cross: "x",
44
+ star: "*",
45
+ square: "[█]",
46
+ square_empty: "[ ]",
47
+ dot: ".",
48
+ bullet: "O",
49
+ bullet_empty: "○",
50
+ marker: ">",
51
+ line: "-",
52
+ pipe: "|",
53
+ ellipsis: "...",
54
+ radio_on: "(*)",
55
+ radio_off: "( )",
56
+ checkbox_on: "[×]",
57
+ checkbox_off: "[ ]",
58
+ circle: "( )",
59
+ circle_on: "(x)",
60
+ circle_off: "( )",
61
+ arrow_up: "↑",
62
+ arrow_down: "↓",
63
+ arrow_up_down: "↕",
64
+ arrow_left: "←",
65
+ arrow_right: "→",
66
+ arrow_left_right: "↔",
67
+ heart: "♥",
68
+ diamond: "♦",
69
+ club: "♣",
70
+ spade: "♠"
71
+ }.freeze
72
+
73
+ def symbols
74
+ @symbols ||= windows? ? WIN_KEYS : KEYS
75
+ end
76
+ module_function :symbols
77
+
78
+ # Check if Windowz
79
+ #
80
+ # @return [Boolean]
81
+ #
82
+ # @api public
83
+ def windows?
84
+ ::File::ALT_SEPARATOR == "\\"
85
+ end
86
+ module_function :windows?
87
+ end # Symbols
88
+ end # Prompt
89
+ end # TTY2
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+
5
+ require_relative "../prompt"
6
+
7
+ module TTY2
8
+ # Used for initializing test cases
9
+ class Prompt
10
+ module StringIOExtensions
11
+ def wait_readable(*)
12
+ true
13
+ end
14
+
15
+ def ioctl(*)
16
+ 80
17
+ end
18
+ end
19
+
20
+ class Test < TTY2::Prompt
21
+ def initialize(**options)
22
+ @input = StringIO.new
23
+ @input.extend(StringIOExtensions)
24
+ @output = StringIO.new
25
+
26
+ options.merge!({
27
+ input: @input,
28
+ output: @output,
29
+ env: { "TTY2_TEST" => true },
30
+ enable_color: options.fetch(:enable_color, true)
31
+ })
32
+ super(**options)
33
+ end
34
+ end # Test
35
+ end # Prompt
36
+ end # TTY2
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTY2
4
+ class Prompt
5
+ class Timer
6
+ attr_reader :duration
7
+
8
+ attr_reader :total
9
+
10
+ attr_reader :interval
11
+
12
+ def initialize(duration, interval)
13
+ @duration = duration
14
+ @interval = interval
15
+ @total = 0.0
16
+ @current = nil
17
+ @events = []
18
+ end
19
+
20
+ def start
21
+ return if @current
22
+
23
+ @current = time_now
24
+ end
25
+
26
+ def stop
27
+ return unless @current
28
+
29
+ @current = nil
30
+ end
31
+
32
+ def runtime
33
+ time_now - @current
34
+ end
35
+
36
+ def on_tick(&block)
37
+ @events << block
38
+ end
39
+
40
+ def while_remaining
41
+ start
42
+ remaining = duration
43
+
44
+ if @duration
45
+ while remaining >= 0.0
46
+ if runtime >= total
47
+ tick = duration - @total
48
+ @events.each { |block| block.(tick) }
49
+ @total += @interval
50
+ end
51
+
52
+ yield(remaining)
53
+ remaining = duration - runtime
54
+ end
55
+ else
56
+ loop { yield }
57
+ end
58
+ ensure
59
+ stop
60
+ end
61
+
62
+ if defined?(Process::CLOCK_MONOTONIC)
63
+ # Object representing current time
64
+ def time_now
65
+ ::Process.clock_gettime(Process::CLOCK_MONOTONIC)
66
+ end
67
+ else
68
+ # Object represeting current time
69
+ def time_now
70
+ ::Time.now
71
+ end
72
+ end
73
+ end # Timer
74
+ end # Prompt
75
+ end # TTY2
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTY2
4
+ module Utils
5
+ module_function
6
+
7
+ BLANK_REGEX = /\A[[:space:]]*\z/o.freeze
8
+
9
+ # Extract options hash from array argument
10
+ #
11
+ # @param [Array[Object]] args
12
+ #
13
+ # @api public
14
+ def extract_options(args)
15
+ options = args.last
16
+ options.respond_to?(:to_hash) ? options.to_hash.dup : {}
17
+ end
18
+
19
+ def extract_options!(args)
20
+ args.last.respond_to?(:to_hash) ? args.pop : {}
21
+ end
22
+
23
+ # Check if value is nil or an empty string
24
+ #
25
+ # @param [Object] value
26
+ # the value to check
27
+ #
28
+ # @return [Boolean]
29
+ #
30
+ # @api public
31
+ def blank?(value)
32
+ value.nil? || BLANK_REGEX === value
33
+ end
34
+
35
+ # Deep copy object
36
+ #
37
+ # @api public
38
+ def deep_copy(object)
39
+ Marshal.load(Marshal.dump(object))
40
+ end
41
+ end # Utils
42
+ end # TTY2
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTY2
4
+ class Prompt
5
+ VERSION = "0.23.1.3"
6
+ end # Prompt
7
+ end # TTY2