clir 0.22.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.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +229 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +44 -0
- data/Manual/Manuel_fr.md +222 -0
- data/Manual/Manuel_fr.pdf +0 -0
- data/README.md +63 -0
- data/REFLEXIONS.md +8 -0
- data/Rakefile +10 -0
- data/TODO.md +9 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/clir.gemspec +40 -0
- data/lib/clir/Array.ext.rb +9 -0
- data/lib/clir/CLI.mod.rb +237 -0
- data/lib/clir/CSV_extension.rb +177 -0
- data/lib/clir/Config.cls.rb +25 -0
- data/lib/clir/Date_utils.rb +161 -0
- data/lib/clir/File_extension.rb +127 -0
- data/lib/clir/Integer.ext.rb +43 -0
- data/lib/clir/Labelizor.rb +231 -0
- data/lib/clir/Replayer.cls.rb +90 -0
- data/lib/clir/String.ext.rb +304 -0
- data/lib/clir/Symbol.ext.rb +20 -0
- data/lib/clir/TTY-Prompt.cls.rb +415 -0
- data/lib/clir/Table.rb +369 -0
- data/lib/clir/console_methods.rb +42 -0
- data/lib/clir/helpers_methods.rb +68 -0
- data/lib/clir/state_methods.rb +48 -0
- data/lib/clir/utils_methods.rb +57 -0
- data/lib/clir/utils_numbers.rb +50 -0
- data/lib/clir/version.rb +3 -0
- data/lib/clir.rb +36 -0
- metadata +136 -0
@@ -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
|
+
|