clir 0.22.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,415 @@
1
+ =begin
2
+
3
+ Class CliTTYPrompt
4
+
5
+ It exposes +Q+ class (with methods) so you can use:
6
+
7
+ `Q.select("Select some vegetables", ...)`
8
+
9
+ This class aims two goals:
10
+
11
+ * make tests easier with CliTest
12
+ * enable the "redo" command (with '_')
13
+
14
+ # To toggle interactive/inputs mode during the tests (mainly)
15
+ TTY::MyPrompt.set_mode_interactive
16
+ TTY::MyPrompt.unset_mode_interactive
17
+
18
+ =end
19
+ require 'tty-prompt'
20
+ require 'json'
21
+
22
+ module TTY
23
+ class MyPrompt < Prompt
24
+
25
+ MARKER_TTY_FILE = File.expand_path(File.join('.','.TTY_MARKER_FILE'))
26
+
27
+ class << self
28
+
29
+ # @prop :interactive or :inputs
30
+ attr_accessor :mode
31
+
32
+ # Methods to switch hardly in interactive mode during tests
33
+ def set_mode_interactive
34
+ Object.send(:remove_const, 'Q')
35
+ Object.const_set('Q', new)
36
+ Q.init(mode_interactive = true)
37
+ File.write(MARKER_TTY_FILE,"#{Time.now}::true")
38
+ end
39
+ def unset_mode_interactive
40
+ Object.send(:remove_const, 'Q')
41
+ Object.const_set('Q', new)
42
+ File.delete(MARKER_TTY_FILE) if File.exist?(MARKER_TTY_FILE)
43
+ Q.init(mode_interactive = false)
44
+ end
45
+ alias :set_mode_inputs :unset_mode_interactive
46
+
47
+ end #/<< self
48
+
49
+ ################### INSTANCE ###################
50
+
51
+
52
+ ##
53
+ # Init Q instance
54
+ #
55
+ def init(mode_interactive = nil)
56
+ if File.exist?(MARKER_TTY_FILE)
57
+ _, mode_interactive = File.read(MARKER_TTY_FILE).split('::')
58
+ mode_interactive = eval(mode_interactive)
59
+ include_methods_by_mode(mode_interactive)
60
+ elsif mode_interactive === nil
61
+ toggle_mode
62
+ else
63
+ include_methods_by_mode(mode_interactive)
64
+ end
65
+ @inputs = nil # for testing
66
+ end
67
+
68
+ # Méthode qui fait basculer du mode normal au mode test et
69
+ # inversement.
70
+ def toggle_mode
71
+ include_methods_by_mode(not(CLI::Replayer.on? || test? ))
72
+ end
73
+
74
+ def include_methods_by_mode(interactive_mode)
75
+ if interactive_mode
76
+ #
77
+ # Usual methods
78
+ # (overwrite tests method if any)
79
+ #
80
+ self.extend ReplayedTTYMethods
81
+ else
82
+ #
83
+ # Use Inputs methods instead of usual methods
84
+ # (overwrite them)
85
+ #
86
+ self.extend InputsTTYMethods
87
+ end
88
+ @mode = interactive_mode ? :interactive : :inputs
89
+ self.class.mode = @mode
90
+ end
91
+
92
+ # Sadly, for select, Tty-prompt requires the :name value for the
93
+ # default value (:default) in methods. This method can return
94
+ # :name valeur from :value
95
+ #
96
+ # @param choices_list {Array} Choices list for select method
97
+ # @param default_value {Any} The default value
98
+ #
99
+ # @return nil or :name of the choice.
100
+ def default_name_for_value(choices_list, default_value)
101
+ choices_list.each do |dchoix|
102
+ if dchoix[:value] == default_value
103
+ return dchoix[:name]
104
+ end
105
+ end
106
+ return nil
107
+ end
108
+
109
+ end #/class MyPrompt
110
+ end
111
+
112
+ ##
113
+ # Regular methods with replay capabilities
114
+ #
115
+ module ReplayedTTYMethods
116
+ class ReplayedPrompt < TTY::Prompt
117
+ # def select(*)
118
+ # eval(code_super(CLI::Replayer.on?))
119
+ # end
120
+
121
+ ##
122
+ # @return method code to evaluate super (if replayer is
123
+ # off) or to get input (if replayer is on)
124
+ def code_super(on)
125
+ CODE_SUPER_OR_GET_INPUT % {truth: on ? 'true' : 'false'}
126
+ end
127
+ CODE_SUPER_OR_GET_INPUT = <<~RUBY
128
+ if %{truth}
129
+ CLI::Replayer.get_input
130
+ else
131
+ super.tap do |result|
132
+ CLI::Replayer.add_input(result)
133
+ end
134
+ end
135
+ RUBY
136
+
137
+ end #/class ReplayedPrompt
138
+ end #/module ReplayedTTYMethods
139
+
140
+ ##
141
+ # Tests methods for TTY::Prompt
142
+ #
143
+ # Each method of TTY::Prompt owns its own method in this
144
+ # module so it can respond to Q.<method> and return the 'inputs'
145
+ # defined in CLITests for user interaction.
146
+ #
147
+ module InputsTTYMethods
148
+
149
+ ##
150
+ # Error class raised when there's no more input values to
151
+ # bring back.
152
+ #
153
+ class CLiTestNoMoreValuesError < StandardError; end
154
+
155
+ ##
156
+ # In test mode, return fake-user inputs (keyboard inputs)
157
+ def inputs
158
+ @inputs ||= begin
159
+ if ENV['CLI_TEST_INPUTS']
160
+ JSON.parse ENV['CLI_TEST_INPUTS']
161
+ elsif CLI::Replayer.inputs
162
+ CLI::Replayer.inputs
163
+ else
164
+ []
165
+ end
166
+ end
167
+ end
168
+
169
+ ##
170
+ # @return next fake-user input or raise a error if no more
171
+ # value to return.
172
+ #
173
+ def next_input
174
+ if inputs.any?
175
+ inputs.shift
176
+ else
177
+ raise CLiTestNoMoreValuesError
178
+ end
179
+ end
180
+
181
+ def response_of(type, *args, &block)
182
+ Responder.new(self, type, *args, &block).response
183
+ end
184
+
185
+ def ask(*args, &block)
186
+ response_of('ask', *args, &block)
187
+ end
188
+ def yes?(*args, &block)
189
+ response_of('yes', *args, &block)
190
+ end
191
+ def no?(*args, &block)
192
+ response_of('no', *args, &block)
193
+ end
194
+ def multiline(*args, &block)
195
+ response_of('multiline', *args, &block)
196
+ end
197
+ def select(*args, &block)
198
+ response_of('select', *args, &block)
199
+ end
200
+ def multi_select(*args, &block)
201
+ response_of('multi_select', *args, &block)
202
+ end
203
+ def slider(*args, &block)
204
+ response_of('slider', *args, &block)
205
+ end
206
+
207
+
208
+ # --- Class InputsTTYMethods::Responder ---
209
+
210
+ class Responder
211
+
212
+ attr_reader :prompt, :type, :args
213
+
214
+ ##
215
+ # The input for this responder
216
+ attr_reader :input
217
+
218
+ def initialize(prompt, type, *args, &block)
219
+ @prompt = prompt
220
+ @type = type
221
+ @args = args
222
+ instance_eval(&block) if block_given?
223
+ end
224
+
225
+ ##
226
+ # Main method to evaluate the respond to give
227
+ # (the respond that user would have given)
228
+ #
229
+ # @return the fake-user input transformed (for example, if no
230
+ # value has been given, return the default value)
231
+ def response
232
+ @input = prompt.next_input
233
+ if treat_special_input_values
234
+ self.send(tty_method)
235
+ end
236
+ return input
237
+ end
238
+
239
+ # --- Special treatment of input values ---
240
+
241
+ ##
242
+ # @return false if no more treatment (no send to tty_method
243
+ # below)
244
+ #
245
+ def treat_special_input_values
246
+ case input.to_s.upcase
247
+ when /CTRL[ _\-]C/, 'EXIT', '^C' then exit 0
248
+ when 'DEFAULT', 'DÉFAUT'
249
+ @input = default_value
250
+ return false
251
+ end
252
+ return true
253
+ end
254
+
255
+ # --- Twin TTY::Prompt methods ---
256
+ # --- They treat input value as required ---
257
+
258
+ def __ask
259
+ # nothing to do (even default value is treated above)
260
+ end
261
+
262
+ def __multiline
263
+ case input
264
+ when /CTRL[ _\-]D/, '^D' then @input = ''
265
+ end
266
+ end
267
+
268
+ def __select
269
+ return unless input.is_a?(Hash)
270
+ @input =
271
+ if input.key?('name')
272
+ find_in_choices(/^#{input['name']}$/i).first
273
+ elsif input.key?('rname')
274
+ find_in_choices(/#{input['rname']}/).first
275
+ elsif input.key?('item') || input.key?('index')
276
+ choices[(input['item']||input['index']) - 1][:value]
277
+ else
278
+ input
279
+ end
280
+ end
281
+
282
+ def __multi_select
283
+ return unless input.is_a?(Hash)
284
+ @input =
285
+ if input.key?('names')
286
+ find_in_choices(/^(#{input['names'].join('|')})$/i)
287
+ elsif input.key?('items') || input.key?('index')
288
+ (input['items']||input['index']).map { |n| choices[n.to_i - 1][:value] }
289
+ elsif input.key?('rname')
290
+ find_in_choices(/#{input['rname']}/i)
291
+ elsif input.key?('rnames')
292
+ find_in_choices(/(#{input['rnames'].join('|')})/i)
293
+ else
294
+ input
295
+ end
296
+ end
297
+
298
+ def __yes
299
+ @input = ['o','y','true',"\n",'1','oui','yes'].include?(input.to_s.downcase)
300
+ end
301
+ def __no
302
+ self.__yes
303
+ @input = !@input
304
+ end
305
+
306
+ def __slider
307
+ @input = input.to_i
308
+ end
309
+
310
+ # --- Usefull methods ---
311
+
312
+ # @return all values that match +search+ in choices
313
+ def find_in_choices(search)
314
+ @choices.select do |choix|
315
+ choix[:name].match?(search)
316
+ end.map do |choix|
317
+ choix[:value]
318
+ end
319
+ end
320
+
321
+ # @return self Twin TTY method
322
+ # p.e. '__ask' ou '__select'
323
+ def tty_method
324
+ @tty_method ||= "__#{type}".to_sym
325
+ end
326
+
327
+ # --- Default value ---
328
+
329
+ ##
330
+ # @return the default value
331
+ def default_value
332
+ if defined?(@default)
333
+ return @default
334
+ elsif args[-1].is_a?(Hash)
335
+ args.last[:default]
336
+ else
337
+ nil
338
+ end
339
+ end
340
+
341
+ # --- Les méthodes communes qui permettent de définir
342
+ # le répondeur ---
343
+
344
+ ##
345
+ # To define and get select choices
346
+ #
347
+ def choices(vals = nil)
348
+ if vals.nil?
349
+ return @choices ||= []
350
+ else
351
+ @choices ||= []
352
+ @choices += vals
353
+ end
354
+ end
355
+
356
+ ##
357
+ # For select or multi-select, to add a choice
358
+ #
359
+ def choice menu, value = nil, options = nil
360
+ @choices ||= []
361
+ @choices << {name:menu, value:value||menu, options:options}
362
+ end
363
+
364
+ ##
365
+ # To define the number of items displayed with a
366
+ # select or multiselect
367
+ #
368
+ def per_page(*args)
369
+ # Rien à faire ici
370
+ end
371
+
372
+ ##
373
+ # To define the default value
374
+ #
375
+ def default(value)
376
+ @default = value
377
+ end
378
+
379
+ ##
380
+ # For real TTYPrompt compatibility
381
+ def enum(*args)
382
+ # Ne rien faire
383
+ end
384
+
385
+ ##
386
+ # For real TTYPrompt compatibility
387
+ def help(str)
388
+ # Ne rien faire
389
+ end
390
+
391
+ ##
392
+ # For real TTYPrompt compatibility
393
+ def validate(*arg, &block)
394
+ # Ne rien faire
395
+ # TODO Plus tard on pourra vérifier les validations aussi
396
+ end
397
+
398
+ ##
399
+ # For real TTYPrompt compatibility
400
+ def convert(*arg, &block)
401
+ # Ne rien faire
402
+ if block_given?
403
+ # TODO Plus tard on pourra vérifier les conversions aussi
404
+ # Mais attention : c'est pas forcément donné par block
405
+ end
406
+ end
407
+
408
+
409
+ end #/class Responder
410
+
411
+ end #/module InputsTTYMethods
412
+
413
+ Q = TTY::MyPrompt.new(symbols: {radio_on:"☒", radio_off:"☐"})
414
+ # Q = TTY::Prompt.new(symbols: {radio_on:"☒", radio_off:"☐"})
415
+