planter-cli 0.0.3

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.
@@ -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