planter-cli 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,352 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Planter
4
+ # Individual question
5
+ module Prompt
6
+ # Class to prompt for answers
7
+ class Question
8
+ attr_reader :key, :type, :min, :max, :prompt, :gum, :condition, :default
9
+
10
+ ##
11
+ ## Initializes the given question.
12
+ ##
13
+ ## @param question [Hash] The question with key, prompt, and type,
14
+ ## optionally default, min and max
15
+ ##
16
+ ## @return [Question] the question object
17
+ ##
18
+ def initialize(question)
19
+ @key = question[:key].to_var
20
+ @type = question[:type].normalize_type
21
+ @min = question[:min]&.to_f || 1.0
22
+ @max = question[:max]&.to_f || 10.0
23
+ @prompt = question[:prompt] || nil
24
+ @default = question[:default]
25
+ @value = question[:value]
26
+ @gum = false # TTY::Which.exist?('gum')
27
+ end
28
+
29
+ ##
30
+ ## Ask the question, prompting for input based on type
31
+ ##
32
+ ## @return [Number, String] the response based on @type
33
+ ##
34
+ def ask
35
+ return nil if @prompt.nil?
36
+
37
+ return @value.to_s.apply_variables.apply_regexes.coerce(@type) if @value
38
+
39
+ res = case @type
40
+ when :integer
41
+ read_number(integer: true)
42
+ when :float
43
+ read_number
44
+ when :date
45
+ if @value
46
+ date_default
47
+ else
48
+ read_date
49
+ end
50
+ when :class || :module
51
+ read_line.to_class_name
52
+ when :multiline
53
+ read_lines
54
+ else
55
+ read_line
56
+ end
57
+ Planter.notify("{dw}#{prompt}: {dy}#{res}{x}", :debug)
58
+ res
59
+ rescue TTY::Reader::InputInterrupt
60
+ raise Errors::InputError('Canceled')
61
+ end
62
+
63
+ private
64
+
65
+ ##
66
+ ## Read a numeric entry using gum or TTY::Reader
67
+ ##
68
+ ## @param integer [Boolean] Round result to nearest integer
69
+ ##
70
+ ## @return [Number] numeric response
71
+ ##
72
+ def read_number(integer: false)
73
+ default = @default ? " {bw}[#{@default}]" : ''
74
+ Planter.notify("{by}#{@prompt} {xc}({bw}#{@min}{xc}-{bw}#{@max}{xc})#{default}")
75
+
76
+ res = @gum ? read_number_gum : read_line_tty
77
+
78
+ return @default unless res
79
+
80
+ res = integer ? res.to_f.round : res.to_f
81
+
82
+ res = read_number if res < @min || res > @max
83
+ res
84
+ end
85
+
86
+ ##
87
+ ## Parse a date value into a date string
88
+ ##
89
+ ## @return [String] date string
90
+ ##
91
+ def date_default
92
+ default = @value || @default
93
+ return nil unless default
94
+
95
+ case default
96
+ when /^(no|ti)/
97
+ Time.now.strftime('%Y-%m-%d %H:%M')
98
+ when /^(to|da)/
99
+ Time.now.strftime('%Y-%m-%d')
100
+ when /^%/
101
+ Time.now.strftime(@default)
102
+ else
103
+ Chronic.parse(default).strftime('%Y-%m-%d')
104
+ end
105
+ end
106
+
107
+ ##
108
+ ## Accept a date string on the command line
109
+ ##
110
+ ## @param prompt [String] The prompt
111
+ ##
112
+ ## @return [Date] Parsed Date object
113
+ ##
114
+ def read_date(prompt: nil)
115
+ prompt ||= @prompt
116
+ default = date_default
117
+
118
+ default = default ? " {bw}[#{default}]" : ''
119
+ Planter.notify("{by}#{prompt} (natural language)#{default}")
120
+ line = @gum ? read_line_gum : read_line_tty
121
+ return default unless line
122
+
123
+ Chronic.parse(line).strftime('%Y-%m-%d')
124
+ end
125
+
126
+ ##
127
+ ## Reads a line.
128
+ ##
129
+ ## @param prompt [String] If not nil, will trigger
130
+ ## asking for a secondary response
131
+ ## until a blank entry is given
132
+ ##
133
+ ## @return [String] the single-line response
134
+ ##
135
+ def read_line(prompt: nil)
136
+ prompt ||= read_lines @prompt
137
+ default = @default ? " {bw}[#{@default}]" : ''
138
+ Planter.notify("{by}#{prompt}#{default}")
139
+
140
+ res = @gum ? read_line_gum : read_line_tty
141
+
142
+ return @default unless res
143
+
144
+ res
145
+ end
146
+
147
+ ##
148
+ ## Reads multiple lines.
149
+ ##
150
+ ## @param prompt [String] if not nil, will trigger
151
+ ## asking for a secondary response
152
+ ## until a blank entry is given
153
+ ##
154
+ ## @return [String] the multi-line response
155
+ ##
156
+ def read_lines(prompt: nil)
157
+ prompt ||= @prompt
158
+ save = @gum ? 'Ctrl-J for newline, Enter' : 'Ctrl-D'
159
+ Planter.notify("{by}#{prompt} {xc}({bw}#{save}{xc} to save)'")
160
+ res = @gum ? read_multiline_gum(prompt) : read_mutliline_tty
161
+
162
+ return @default unless res
163
+
164
+ res.strip
165
+ end
166
+
167
+ ##
168
+ ## Read a numeric entry using gum
169
+ ##
170
+ ## @return [String] String response
171
+ ##
172
+ def read_number_gum
173
+ trap('SIGINT') { exit! }
174
+ res = `gum input --placeholder "#{@min}-#{@max}"`.strip
175
+ return nil if res.strip.empty?
176
+
177
+ res
178
+ end
179
+
180
+ ##
181
+ ## Read a single line entry using TTY::Reader
182
+ ##
183
+ ## @return [String] String response
184
+ ##
185
+ def read_line_tty
186
+ trap('SIGINT') { exit! }
187
+ reader = TTY::Reader.new
188
+ res = reader.read_line('>> ').strip
189
+ return nil if res.empty?
190
+
191
+ res
192
+ end
193
+
194
+ ##
195
+ ## Read a single line entry using gum
196
+ ##
197
+ ## @return [String] String response
198
+ ##
199
+ def read_line_gum
200
+ trap('SIGINT') { exit! }
201
+ res = `gum input --placeholder "(blank to use default)"`.strip
202
+ return nil if res.empty?
203
+
204
+ res
205
+ end
206
+
207
+ ##
208
+ ## Read a multiline entry using TTY::Reader
209
+ ##
210
+ ## @return [string] multiline input
211
+ ##
212
+ def read_mutliline_tty
213
+ trap('SIGINT') { exit! }
214
+ reader = TTY::Reader.new
215
+ res = reader.read_multiline
216
+ res.join("\n").strip
217
+ end
218
+
219
+ ##
220
+ ## Read a multiline entry using gum
221
+ ##
222
+ ## @return [string] multiline input
223
+ ##
224
+ def read_multiline_gum(prompt)
225
+ trap('SIGINT') { exit! }
226
+ width = TTY::Screen.cols > 80 ? 80 : TTY::Screen.cols
227
+ `gum write --placeholder "#{prompt}" --width #{width} --char-limit 0`.strip
228
+ end
229
+ end
230
+
231
+ ##
232
+ ## Choose from an array of multiple choices. Letter surrounded in
233
+ ## parenthesis becomes character for response. Only one letter should be
234
+ ## specified and must be unique.
235
+ ##
236
+ ## @param choices [Array] The choices
237
+ ## @param prompt [String] The prompt
238
+ ## @param default_response [String] The character of the default
239
+ ## response
240
+ ##
241
+ ## @return [String] character of selected response, lowercased
242
+ ##
243
+ def self.choice(choices, prompt = 'Make a selection', default_response: nil)
244
+ $stdin.reopen('/dev/tty')
245
+
246
+ default = default_response.is_a?(String) ? default_response.downcase : nil
247
+
248
+ # if this isn't an interactive shell, answer default
249
+ return default unless $stdout.isatty
250
+
251
+ # If --defaults is set, return default
252
+ return default if Planter.accept_defaults
253
+
254
+ # clear the buffer
255
+ if ARGV&.length
256
+ ARGV.length.times do
257
+ ARGV.shift
258
+ end
259
+ end
260
+ system 'stty cbreak'
261
+
262
+ vertical = choices.join(' ').length + 4 > TTY::Screen.cols
263
+ desc = choices.map { |c| c.highlight_character(default: default) }
264
+ abbr = choices.abbr_choices(default: default)
265
+
266
+ options = if vertical
267
+ "{x}#{desc.join("\n")}\n{by}#{prompt}{x} #{abbr}{bw}? "
268
+ else
269
+ "{by}#{prompt}{bw}?\n#{desc.join(', ')}{x} #{abbr}:{x} "
270
+ end
271
+
272
+ $stdout.syswrite options.x
273
+ res = $stdin.sysread 1
274
+ puts
275
+ system 'stty cooked'
276
+
277
+ res.chomp!
278
+ res.downcase!
279
+
280
+ res.empty? ? default : res
281
+ end
282
+
283
+ def self.file_what?(entry)
284
+ options = %w[(o)vewrite (m)erge]
285
+ options << '(c)opy' unless File.exist?(entry.target)
286
+ options << '(i)gnore'
287
+ opt = Prompt.choice(options, "What do you want to do with #{File.basename(entry.target)}", default_response: 'i')
288
+ case opt
289
+ when /^m/
290
+ :merge
291
+ when /^o/
292
+ :overwrite
293
+ when /^c/
294
+ :copy
295
+ else
296
+ :ignore
297
+ end
298
+ end
299
+
300
+ ##
301
+ ## Ask a yes or no question in the terminal
302
+ ##
303
+ ## @param question [String] The question
304
+ ## to ask
305
+ ## @param default_response [Boolean] default
306
+ ## response if no input
307
+ ##
308
+ ## @return [Boolean] yes or no
309
+ ##
310
+ def self.yn(question, default_response: false)
311
+ $stdin.reopen('/dev/tty')
312
+
313
+ default = if default_response.is_a?(String)
314
+ default_response =~ /y/i ? true : false
315
+ else
316
+ default_response
317
+ end
318
+
319
+ # if this isn't an interactive shell, answer default
320
+ return default unless $stdout.isatty
321
+
322
+ # If --defaults is set, return default
323
+ return default if Planter.accept_defaults
324
+
325
+ # clear the buffer
326
+ if ARGV&.length
327
+ ARGV.length.times do
328
+ ARGV.shift
329
+ end
330
+ end
331
+ system 'stty cbreak'
332
+
333
+ options = if default.nil?
334
+ '{w}[{bw}y{w}/{bw}n{w}]'
335
+ else
336
+ "{w}[#{default ? '{bg}Y{w}/{bw}n' : '{bw}y{w}/{bg}N'}{w}]"
337
+ end
338
+
339
+ $stdout.syswrite "{bw}#{question.sub(/\?$/, '')} #{options}{bw}? {x}".x
340
+ res = $stdin.sysread 1
341
+ puts
342
+ system 'stty cooked'
343
+
344
+ res.chomp!
345
+ res.downcase!
346
+
347
+ return default if res.empty?
348
+
349
+ res =~ /y/i ? true : false
350
+ end
351
+ end
352
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Planter
4
+ # Script handler
5
+ class Script
6
+ attr_reader :script
7
+
8
+ ##
9
+ ## Initialize a Script object
10
+ ##
11
+ ## @param template_dir [String] Path to the current template dir
12
+ ## @param output_dir [String] The planted template directory
13
+ ## @param script [String] The script name
14
+ ##
15
+ def initialize(template_dir, output_dir, script)
16
+ found = find_script(template_dir, output_dir, script)
17
+ Planter.notify("Script #{script} not found", :error, exit_code: 10) unless found
18
+ @script = found
19
+
20
+ Planter.notify("Directory #{output_dir} not found", :error, exit_code: 10) unless File.directory?(output_dir)
21
+ @template_directory = template_dir
22
+ @directory = output_dir
23
+ end
24
+
25
+ ##
26
+ ## Locate a script in either the base directory or template directory
27
+ ##
28
+ ## @param template_dir [String] The template dir
29
+ ## @param script [String] The script name
30
+ ##
31
+ ## @return [String] Path to script
32
+ ##
33
+ def find_script(template_dir, script)
34
+ parts = Shellwords.split(script).first
35
+ return script if File.exist?(parts[0])
36
+
37
+ if File.exist?(File.join(template_dir, '_scripts', parts[0]))
38
+ return "#{File.join(template_dir, '_scripts', parts[0])} #{parts[1..-1]}"
39
+ elsif File.exist?(File.join(BASE_DIR, 'scripts', parts[0]))
40
+ return "#{File.join(BASE_DIR, 'scripts', parts[0])} #{parts[1..-1]}"
41
+ end
42
+
43
+ nil
44
+ end
45
+
46
+ ##
47
+ ## Execute script, passing template directory and output directory as arguments $1 and $2
48
+ ##
49
+ ## @return [Boolean] true if success?
50
+ ##
51
+ def run
52
+ `#{@script} "#{@template_directory}" "#{@directory}"`
53
+
54
+ Planter.notify("Error running #{File.basename(@script)}", :error, exit_code: 128) unless $?.success?
55
+
56
+ true
57
+ end
58
+ end
59
+ end