clack 0.1.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/CHANGELOG.md +17 -0
- data/LICENSE +24 -0
- data/README.md +424 -0
- data/exe/clack-demo +9 -0
- data/lib/clack/box.rb +120 -0
- data/lib/clack/colors.rb +55 -0
- data/lib/clack/core/cursor.rb +61 -0
- data/lib/clack/core/key_reader.rb +45 -0
- data/lib/clack/core/options_helper.rb +96 -0
- data/lib/clack/core/prompt.rb +215 -0
- data/lib/clack/core/settings.rb +97 -0
- data/lib/clack/core/text_input_helper.rb +83 -0
- data/lib/clack/environment.rb +137 -0
- data/lib/clack/group.rb +100 -0
- data/lib/clack/log.rb +42 -0
- data/lib/clack/note.rb +49 -0
- data/lib/clack/prompts/autocomplete.rb +162 -0
- data/lib/clack/prompts/autocomplete_multiselect.rb +280 -0
- data/lib/clack/prompts/confirm.rb +100 -0
- data/lib/clack/prompts/group_multiselect.rb +250 -0
- data/lib/clack/prompts/multiselect.rb +185 -0
- data/lib/clack/prompts/password.rb +77 -0
- data/lib/clack/prompts/path.rb +226 -0
- data/lib/clack/prompts/progress.rb +145 -0
- data/lib/clack/prompts/select.rb +134 -0
- data/lib/clack/prompts/select_key.rb +100 -0
- data/lib/clack/prompts/spinner.rb +206 -0
- data/lib/clack/prompts/tasks.rb +131 -0
- data/lib/clack/prompts/text.rb +93 -0
- data/lib/clack/stream.rb +82 -0
- data/lib/clack/symbols.rb +84 -0
- data/lib/clack/task_log.rb +174 -0
- data/lib/clack/utils.rb +135 -0
- data/lib/clack/validators.rb +145 -0
- data/lib/clack/version.rb +5 -0
- data/lib/clack.rb +576 -0
- metadata +83 -0
data/lib/clack.rb
ADDED
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "clack/version"
|
|
4
|
+
require_relative "clack/symbols"
|
|
5
|
+
require_relative "clack/colors"
|
|
6
|
+
require_relative "clack/environment"
|
|
7
|
+
require_relative "clack/utils"
|
|
8
|
+
require_relative "clack/core/cursor"
|
|
9
|
+
require_relative "clack/core/settings"
|
|
10
|
+
require_relative "clack/core/key_reader"
|
|
11
|
+
require_relative "clack/core/prompt"
|
|
12
|
+
require_relative "clack/core/options_helper"
|
|
13
|
+
require_relative "clack/core/text_input_helper"
|
|
14
|
+
require_relative "clack/prompts/text"
|
|
15
|
+
require_relative "clack/prompts/password"
|
|
16
|
+
require_relative "clack/prompts/confirm"
|
|
17
|
+
require_relative "clack/prompts/select"
|
|
18
|
+
require_relative "clack/prompts/multiselect"
|
|
19
|
+
require_relative "clack/prompts/spinner"
|
|
20
|
+
require_relative "clack/prompts/autocomplete"
|
|
21
|
+
require_relative "clack/prompts/autocomplete_multiselect"
|
|
22
|
+
require_relative "clack/prompts/path"
|
|
23
|
+
require_relative "clack/prompts/progress"
|
|
24
|
+
require_relative "clack/prompts/select_key"
|
|
25
|
+
require_relative "clack/prompts/tasks"
|
|
26
|
+
require_relative "clack/prompts/group_multiselect"
|
|
27
|
+
require_relative "clack/log"
|
|
28
|
+
require_relative "clack/note"
|
|
29
|
+
require_relative "clack/box"
|
|
30
|
+
require_relative "clack/group"
|
|
31
|
+
require_relative "clack/stream"
|
|
32
|
+
require_relative "clack/task_log"
|
|
33
|
+
require_relative "clack/validators"
|
|
34
|
+
|
|
35
|
+
# Clack - Beautiful CLI prompts for Ruby
|
|
36
|
+
#
|
|
37
|
+
# A faithful Ruby port of @clack/prompts, bringing delightful terminal
|
|
38
|
+
# aesthetics to your Ruby projects.
|
|
39
|
+
#
|
|
40
|
+
# @example Basic usage
|
|
41
|
+
# Clack.intro "Welcome to my-app"
|
|
42
|
+
# name = Clack.text(message: "What's your name?")
|
|
43
|
+
# exit 1 if Clack.cancel?(name)
|
|
44
|
+
# Clack.outro "Nice to meet you, #{name}!"
|
|
45
|
+
#
|
|
46
|
+
# @example Using prompt groups
|
|
47
|
+
# result = Clack.group do |g|
|
|
48
|
+
# g.prompt(:name) { Clack.text(message: "Name?") }
|
|
49
|
+
# g.prompt(:confirm) { Clack.confirm(message: "Continue?") }
|
|
50
|
+
# end
|
|
51
|
+
#
|
|
52
|
+
# @see https://github.com/bombshell-dev/clack Original JavaScript library
|
|
53
|
+
module Clack
|
|
54
|
+
# Sentinel value returned when user cancels a prompt (Escape or Ctrl+C)
|
|
55
|
+
CANCEL = Object.new.tap { |o| o.define_singleton_method(:inspect) { "Clack::CANCEL" } }.freeze
|
|
56
|
+
|
|
57
|
+
class << self
|
|
58
|
+
# Check if a prompt result was cancelled by the user.
|
|
59
|
+
#
|
|
60
|
+
# @param value [Object] the result from a prompt
|
|
61
|
+
# @return [Boolean] true if the user cancelled
|
|
62
|
+
def cancel?(value)
|
|
63
|
+
value.equal?(CANCEL)
|
|
64
|
+
end
|
|
65
|
+
alias_method :cancelled?, :cancel?
|
|
66
|
+
|
|
67
|
+
# Check if cancelled and show cancel message if so.
|
|
68
|
+
# Useful for guard clauses in CLI scripts.
|
|
69
|
+
#
|
|
70
|
+
# @param value [Object] the result from a prompt
|
|
71
|
+
# @param message [String] message to display if cancelled
|
|
72
|
+
# @param output [IO] output stream
|
|
73
|
+
# @return [Boolean] true if cancelled
|
|
74
|
+
#
|
|
75
|
+
# @example Guard clause pattern
|
|
76
|
+
# name = Clack.text(message: "Name?")
|
|
77
|
+
# return if Clack.handle_cancel(name) # Shows "Cancelled" and returns true
|
|
78
|
+
#
|
|
79
|
+
# @example With custom message
|
|
80
|
+
# return if Clack.handle_cancel(name, "Aborted by user")
|
|
81
|
+
def handle_cancel(value, message = "Cancelled", output: $stdout)
|
|
82
|
+
return false unless cancel?(value)
|
|
83
|
+
|
|
84
|
+
cancel(message, output: output)
|
|
85
|
+
true
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Display an intro banner at the start of a CLI session.
|
|
89
|
+
#
|
|
90
|
+
# @param title [String, nil] optional title text
|
|
91
|
+
# @param output [IO] output stream (default: $stdout)
|
|
92
|
+
# @return [void]
|
|
93
|
+
def intro(title = nil, output: $stdout)
|
|
94
|
+
output.puts "#{Colors.gray(Symbols::S_BAR_START)} #{title}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Display an outro banner at the end of a CLI session.
|
|
98
|
+
#
|
|
99
|
+
# @param message [String, nil] optional closing message
|
|
100
|
+
# @param output [IO] output stream (default: $stdout)
|
|
101
|
+
# @return [void]
|
|
102
|
+
def outro(message = nil, output: $stdout)
|
|
103
|
+
output.puts Colors.gray(Symbols::S_BAR)
|
|
104
|
+
output.puts "#{Colors.gray(Symbols::S_BAR_END)} #{message}"
|
|
105
|
+
output.puts
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Display a cancellation message (typically after user presses Escape).
|
|
109
|
+
#
|
|
110
|
+
# @param message [String, nil] optional cancellation message
|
|
111
|
+
# @param output [IO] output stream (default: $stdout)
|
|
112
|
+
# @return [void]
|
|
113
|
+
def cancel(message = nil, output: $stdout)
|
|
114
|
+
output.puts Colors.gray(Symbols::S_BAR)
|
|
115
|
+
output.puts "#{Colors.gray(Symbols::S_BAR_END)} #{Colors.red(message)}"
|
|
116
|
+
output.puts
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Prompt for single-line text input.
|
|
120
|
+
#
|
|
121
|
+
# @param message [String] the prompt message
|
|
122
|
+
# @param placeholder [String, nil] dim text shown when input is empty
|
|
123
|
+
# @param default_value [String, nil] value used if submitted empty
|
|
124
|
+
# @param initial_value [String, nil] pre-filled editable text
|
|
125
|
+
# @param validate [Proc, nil] validation function returning error message or nil
|
|
126
|
+
# @return [String, CANCEL] user input or CANCEL if cancelled
|
|
127
|
+
def text(message:, **opts)
|
|
128
|
+
Prompts::Text.new(message:, **opts).run
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Prompt for password input (masked display).
|
|
132
|
+
#
|
|
133
|
+
# @param message [String] the prompt message
|
|
134
|
+
# @param mask [String] character to display for each input character (default: ▪)
|
|
135
|
+
# @param validate [Proc, nil] validation function
|
|
136
|
+
# @return [String, CANCEL] password or CANCEL if cancelled
|
|
137
|
+
def password(message:, **opts)
|
|
138
|
+
Prompts::Password.new(message:, **opts).run
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Prompt for yes/no confirmation.
|
|
142
|
+
#
|
|
143
|
+
# @param message [String] the prompt message
|
|
144
|
+
# @param active [String] label for "yes" option (default: "Yes")
|
|
145
|
+
# @param inactive [String] label for "no" option (default: "No")
|
|
146
|
+
# @param initial_value [Boolean] default selection (default: true)
|
|
147
|
+
# @return [Boolean, CANCEL] true/false or CANCEL if cancelled
|
|
148
|
+
def confirm(message:, **opts)
|
|
149
|
+
Prompts::Confirm.new(message:, **opts).run
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Prompt to select one option from a list.
|
|
153
|
+
#
|
|
154
|
+
# @param message [String] the prompt message
|
|
155
|
+
# @param options [Array<Hash, String>] list of options
|
|
156
|
+
# @param initial_value [Object, nil] value of initially selected option
|
|
157
|
+
# @param max_items [Integer, nil] max visible items (enables scrolling)
|
|
158
|
+
# @return [Object, CANCEL] selected value or CANCEL if cancelled
|
|
159
|
+
def select(message:, options:, **opts)
|
|
160
|
+
Prompts::Select.new(message:, options: options, **opts).run
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Prompt to select multiple options from a list.
|
|
164
|
+
#
|
|
165
|
+
# @param message [String] the prompt message
|
|
166
|
+
# @param options [Array<Hash, String>] list of options
|
|
167
|
+
# @param initial_values [Array, nil] initially selected values
|
|
168
|
+
# @param required [Boolean] require at least one selection (default: true)
|
|
169
|
+
# @param max_items [Integer, nil] max visible items (enables scrolling)
|
|
170
|
+
# @return [Array, CANCEL] selected values or CANCEL if cancelled
|
|
171
|
+
def multiselect(message:, options:, **opts)
|
|
172
|
+
Prompts::Multiselect.new(message:, options: options, **opts).run
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Create an animated spinner for async operations.
|
|
176
|
+
#
|
|
177
|
+
# @return [Prompts::Spinner] spinner instance (call #start, #stop, #error)
|
|
178
|
+
def spinner(**opts)
|
|
179
|
+
Prompts::Spinner.new(**opts)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Run a block with a spinner, handling success/error automatically.
|
|
183
|
+
#
|
|
184
|
+
# @param message [String] initial spinner message
|
|
185
|
+
# @param success_message [String, nil] message on success (defaults to message)
|
|
186
|
+
# @param error_message [String, nil] message on error (defaults to exception message)
|
|
187
|
+
# @return [Object] the block's return value
|
|
188
|
+
# @raise [Exception] re-raises any exception from the block
|
|
189
|
+
#
|
|
190
|
+
# @example Basic usage
|
|
191
|
+
# result = Clack.spin("Installing dependencies...") { system("npm install") }
|
|
192
|
+
#
|
|
193
|
+
# @example With custom success message
|
|
194
|
+
# Clack.spin("Compiling...", success: "Build complete!") { build_project }
|
|
195
|
+
#
|
|
196
|
+
# @example Access spinner inside block
|
|
197
|
+
# Clack.spin("Working...") do |s|
|
|
198
|
+
# s.message "Step 1..."
|
|
199
|
+
# do_step_1
|
|
200
|
+
# s.message "Step 2..."
|
|
201
|
+
# do_step_2
|
|
202
|
+
# end
|
|
203
|
+
def spin(message, success: nil, error: nil, **opts)
|
|
204
|
+
s = spinner(**opts)
|
|
205
|
+
s.start(message)
|
|
206
|
+
begin
|
|
207
|
+
result = yield(s)
|
|
208
|
+
s.stop(success || message)
|
|
209
|
+
result
|
|
210
|
+
rescue => exception
|
|
211
|
+
s.error(error || exception.message)
|
|
212
|
+
raise
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Prompt with type-to-filter autocomplete.
|
|
217
|
+
#
|
|
218
|
+
# @param message [String] the prompt message
|
|
219
|
+
# @param options [Array<Hash, String>] list of options to filter
|
|
220
|
+
# @param placeholder [String, nil] placeholder text
|
|
221
|
+
# @return [Object, CANCEL] selected value or CANCEL if cancelled
|
|
222
|
+
def autocomplete(message:, options:, **opts)
|
|
223
|
+
Prompts::Autocomplete.new(message:, options: options, **opts).run
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Prompt with type-to-filter autocomplete and multiselect.
|
|
227
|
+
#
|
|
228
|
+
# @param message [String] the prompt message
|
|
229
|
+
# @param options [Array<Hash, String>] list of options to filter
|
|
230
|
+
# @param placeholder [String, nil] placeholder text
|
|
231
|
+
# @param required [Boolean] require at least one selection (default: true)
|
|
232
|
+
# @param initial_values [Array, nil] initially selected values
|
|
233
|
+
# @return [Array, CANCEL] selected values or CANCEL if cancelled
|
|
234
|
+
def autocomplete_multiselect(message:, options:, **opts)
|
|
235
|
+
Prompts::AutocompleteMultiselect.new(message:, options: options, **opts).run
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Prompt for file/directory path with filesystem navigation.
|
|
239
|
+
#
|
|
240
|
+
# @param message [String] the prompt message
|
|
241
|
+
# @param root [String] starting directory (default: ".")
|
|
242
|
+
# @param only_directories [Boolean] only show directories (default: false)
|
|
243
|
+
# @return [String, CANCEL] selected path or CANCEL if cancelled
|
|
244
|
+
def path(message:, **opts)
|
|
245
|
+
Prompts::Path.new(message:, **opts).run
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Create a progress bar for measurable operations.
|
|
249
|
+
#
|
|
250
|
+
# @param total [Integer] total number of steps
|
|
251
|
+
# @param message [String, nil] optional message
|
|
252
|
+
# @return [Prompts::Progress] progress instance (call #start, #advance, #stop)
|
|
253
|
+
def progress(total:, **opts)
|
|
254
|
+
Prompts::Progress.new(total: total, **opts)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Prompt to select an option by pressing a key.
|
|
258
|
+
#
|
|
259
|
+
# @param message [String] the prompt message
|
|
260
|
+
# @param options [Array<Hash>] options with :value, :label, and :key
|
|
261
|
+
# @return [Object, CANCEL] selected value or CANCEL if cancelled
|
|
262
|
+
def select_key(message:, options:, **opts)
|
|
263
|
+
Prompts::SelectKey.new(message:, options: options, **opts).run
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Run multiple tasks with progress indicators.
|
|
267
|
+
#
|
|
268
|
+
# @param tasks [Array<Hash>] tasks with :title and :task (Proc)
|
|
269
|
+
# @return [Array<Hash>] task results
|
|
270
|
+
def tasks(tasks:, **opts)
|
|
271
|
+
Prompts::Tasks.new(tasks: tasks, **opts).run
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Prompt to select multiple options organized in groups.
|
|
275
|
+
#
|
|
276
|
+
# @param message [String] the prompt message
|
|
277
|
+
# @param options [Array<Hash>] groups with :label and :options
|
|
278
|
+
# @param initial_values [Array, nil] initially selected values
|
|
279
|
+
# @param required [Boolean] require at least one selection (default: true)
|
|
280
|
+
# @return [Array, CANCEL] selected values or CANCEL if cancelled
|
|
281
|
+
def group_multiselect(message:, options:, **opts)
|
|
282
|
+
Prompts::GroupMultiselect.new(message:, options: options, **opts).run
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Access the Log module for styled console output.
|
|
286
|
+
#
|
|
287
|
+
# @return [Module] the Log module
|
|
288
|
+
def log
|
|
289
|
+
Log
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Access the Stream module for streaming output.
|
|
293
|
+
#
|
|
294
|
+
# @return [Module] the Stream module
|
|
295
|
+
def stream
|
|
296
|
+
Stream
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Display a note box with optional title.
|
|
300
|
+
#
|
|
301
|
+
# @param message [String] the note content
|
|
302
|
+
# @param title [String, nil] optional title
|
|
303
|
+
# @return [void]
|
|
304
|
+
def note(message = "", title: nil, **opts)
|
|
305
|
+
Note.render(message, title: title, **opts)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Display content in a customizable box
|
|
309
|
+
# @param message [String] the box content
|
|
310
|
+
# @param title [String] optional title
|
|
311
|
+
# @param content_align [:left, :center, :right] content alignment
|
|
312
|
+
# @param title_align [:left, :center, :right] title alignment
|
|
313
|
+
# @param width [Integer, :auto] box width
|
|
314
|
+
# @param rounded [Boolean] use rounded corners
|
|
315
|
+
# @return [void]
|
|
316
|
+
def box(message = "", title: "", **opts)
|
|
317
|
+
Box.render(message, title: title, **opts)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Create a streaming task log that clears on success, shows on error.
|
|
321
|
+
# Useful for build output, npm install style streaming, etc.
|
|
322
|
+
#
|
|
323
|
+
# @param title [String] title displayed at the top
|
|
324
|
+
# @param limit [Integer, nil] max lines to show (older lines scroll out)
|
|
325
|
+
# @param retain_log [Boolean] keep full log history for display on error
|
|
326
|
+
# @return [TaskLog] task log instance
|
|
327
|
+
def task_log(title:, **opts)
|
|
328
|
+
TaskLog.new(title: title, **opts)
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Access global settings
|
|
332
|
+
# @return [Hash] Current configuration
|
|
333
|
+
# @see Core::Settings.update for modifying settings
|
|
334
|
+
def settings
|
|
335
|
+
Core::Settings.config
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Update global settings
|
|
339
|
+
# @param aliases [Hash, nil] Custom key to action mappings
|
|
340
|
+
# @param with_guide [Boolean, nil] Whether to show guide bars
|
|
341
|
+
# @return [Hash] Updated configuration
|
|
342
|
+
#
|
|
343
|
+
# @example Custom key bindings
|
|
344
|
+
# Clack.update_settings(aliases: { "y" => :enter, "n" => :cancel })
|
|
345
|
+
#
|
|
346
|
+
# @example Disable guide bars
|
|
347
|
+
# Clack.update_settings(with_guide: false)
|
|
348
|
+
def update_settings(**opts)
|
|
349
|
+
Core::Settings.update(**opts)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Check if running in a CI environment
|
|
353
|
+
# @return [Boolean]
|
|
354
|
+
def ci?
|
|
355
|
+
Environment.ci?
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Check if running on Windows
|
|
359
|
+
# @return [Boolean]
|
|
360
|
+
def windows?
|
|
361
|
+
Environment.windows?
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Check if stdout is a TTY
|
|
365
|
+
# @param output [IO] Output stream to check
|
|
366
|
+
# @return [Boolean]
|
|
367
|
+
def tty?(output = $stdout)
|
|
368
|
+
Environment.tty?(output)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# Get terminal columns (width)
|
|
372
|
+
# @param output [IO] Output stream
|
|
373
|
+
# @param default [Integer] Default if detection fails
|
|
374
|
+
# @return [Integer]
|
|
375
|
+
def columns(output = $stdout, default: 80)
|
|
376
|
+
Environment.columns(output, default: default)
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Get terminal rows (height)
|
|
380
|
+
# @param output [IO] Output stream
|
|
381
|
+
# @param default [Integer] Default if detection fails
|
|
382
|
+
# @return [Integer]
|
|
383
|
+
def rows(output = $stdout, default: 24)
|
|
384
|
+
Environment.rows(output, default: default)
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# :nocov:
|
|
388
|
+
# :reek:TooManyStatements :reek:NestedIterators :reek:UncommunicativeVariableName
|
|
389
|
+
# Demo - showcases all Clack features (interactive, tested manually)
|
|
390
|
+
def demo
|
|
391
|
+
intro "clack-demo"
|
|
392
|
+
|
|
393
|
+
result = group(on_cancel: ->(_) { cancel("Operation cancelled.") }) do |g|
|
|
394
|
+
g.prompt(:name) do
|
|
395
|
+
text(
|
|
396
|
+
message: "What is your project named?",
|
|
397
|
+
placeholder: "my-app",
|
|
398
|
+
validate: ->(v) { "Project name is required" if v.to_s.strip.empty? }
|
|
399
|
+
)
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
g.prompt(:directory) do |r|
|
|
403
|
+
text(
|
|
404
|
+
message: "Where should we create your project?",
|
|
405
|
+
initial_value: "./#{r[:name]}"
|
|
406
|
+
)
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
g.prompt(:template) do
|
|
410
|
+
select(
|
|
411
|
+
message: "Which template would you like to use?",
|
|
412
|
+
options: [
|
|
413
|
+
{value: "default", label: "Default", hint: "recommended"},
|
|
414
|
+
{value: "minimal", label: "Minimal", hint: "bare bones"},
|
|
415
|
+
{value: "api", label: "API Only", hint: "no frontend"},
|
|
416
|
+
{value: "full", label: "Full Stack", hint: "everything included"}
|
|
417
|
+
]
|
|
418
|
+
)
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
g.prompt(:typescript) do
|
|
422
|
+
confirm(
|
|
423
|
+
message: "Would you like to use TypeScript?",
|
|
424
|
+
initial_value: true
|
|
425
|
+
)
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
g.prompt(:features) do
|
|
429
|
+
multiselect(
|
|
430
|
+
message: "Which features would you like to include?",
|
|
431
|
+
options: [
|
|
432
|
+
{value: "eslint", label: "ESLint", hint: "code linting"},
|
|
433
|
+
{value: "prettier", label: "Prettier", hint: "code formatting"},
|
|
434
|
+
{value: "tailwind", label: "Tailwind CSS", hint: "utility-first CSS"},
|
|
435
|
+
{value: "docker", label: "Docker", hint: "containerization"},
|
|
436
|
+
{value: "ci", label: "GitHub Actions", hint: "CI/CD pipeline"}
|
|
437
|
+
],
|
|
438
|
+
initial_values: %w[eslint prettier],
|
|
439
|
+
required: false
|
|
440
|
+
)
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
g.prompt(:package_manager) do
|
|
444
|
+
select(
|
|
445
|
+
message: "Which package manager do you prefer?",
|
|
446
|
+
options: [
|
|
447
|
+
{value: "npm", label: "npm"},
|
|
448
|
+
{value: "yarn", label: "yarn"},
|
|
449
|
+
{value: "pnpm", label: "pnpm", hint: "recommended"},
|
|
450
|
+
{value: "bun", label: "bun", hint: "fast"}
|
|
451
|
+
],
|
|
452
|
+
initial_value: "pnpm"
|
|
453
|
+
)
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
return if cancel?(result)
|
|
458
|
+
|
|
459
|
+
# Autocomplete prompt
|
|
460
|
+
color = autocomplete(
|
|
461
|
+
message: "Pick a theme color:",
|
|
462
|
+
options: %w[red orange yellow green blue indigo violet pink cyan magenta]
|
|
463
|
+
)
|
|
464
|
+
return if handle_cancel(color)
|
|
465
|
+
|
|
466
|
+
# Select key prompt (quick keyboard shortcuts)
|
|
467
|
+
action = select_key(
|
|
468
|
+
message: "What would you like to do first?",
|
|
469
|
+
options: [
|
|
470
|
+
{value: "dev", label: "Start dev server", key: "d"},
|
|
471
|
+
{value: "build", label: "Build for production", key: "b"},
|
|
472
|
+
{value: "test", label: "Run tests", key: "t"}
|
|
473
|
+
]
|
|
474
|
+
)
|
|
475
|
+
return if handle_cancel(action)
|
|
476
|
+
|
|
477
|
+
# Path prompt
|
|
478
|
+
config_path = path(
|
|
479
|
+
message: "Select config directory:",
|
|
480
|
+
only_directories: true
|
|
481
|
+
)
|
|
482
|
+
return if handle_cancel(config_path)
|
|
483
|
+
|
|
484
|
+
# Group multiselect
|
|
485
|
+
stack = group_multiselect(
|
|
486
|
+
message: "Select additional integrations:",
|
|
487
|
+
options: [
|
|
488
|
+
{
|
|
489
|
+
label: "Frontend",
|
|
490
|
+
options: [
|
|
491
|
+
{value: "react", label: "React"},
|
|
492
|
+
{value: "vue", label: "Vue"},
|
|
493
|
+
{value: "svelte", label: "Svelte"}
|
|
494
|
+
]
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
label: "Backend",
|
|
498
|
+
options: [
|
|
499
|
+
{value: "express", label: "Express"},
|
|
500
|
+
{value: "fastify", label: "Fastify"},
|
|
501
|
+
{value: "hono", label: "Hono"}
|
|
502
|
+
]
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
label: "Database",
|
|
506
|
+
options: [
|
|
507
|
+
{value: "postgres", label: "PostgreSQL"},
|
|
508
|
+
{value: "mysql", label: "MySQL"},
|
|
509
|
+
{value: "sqlite", label: "SQLite"}
|
|
510
|
+
]
|
|
511
|
+
}
|
|
512
|
+
],
|
|
513
|
+
required: false
|
|
514
|
+
)
|
|
515
|
+
return if handle_cancel(stack)
|
|
516
|
+
|
|
517
|
+
# Progress bar
|
|
518
|
+
prog = progress(total: 100, message: "Downloading assets...")
|
|
519
|
+
prog.start
|
|
520
|
+
20.times do
|
|
521
|
+
sleep 0.03
|
|
522
|
+
prog.advance(5)
|
|
523
|
+
end
|
|
524
|
+
prog.stop("Assets downloaded!")
|
|
525
|
+
|
|
526
|
+
# Tasks
|
|
527
|
+
tasks(tasks: [
|
|
528
|
+
{title: "Validating configuration", task: -> { sleep 0.3 }},
|
|
529
|
+
{title: "Generating types", task: -> { sleep 0.4 }},
|
|
530
|
+
{title: "Compiling assets", task: -> { sleep 0.3 }}
|
|
531
|
+
])
|
|
532
|
+
|
|
533
|
+
# Spinner
|
|
534
|
+
s = spinner
|
|
535
|
+
s.start "Installing dependencies via #{result[:package_manager]}..."
|
|
536
|
+
sleep 1.0
|
|
537
|
+
s.message "Configuring #{result[:template]} template..."
|
|
538
|
+
sleep 0.6
|
|
539
|
+
s.stop "Project created successfully!"
|
|
540
|
+
|
|
541
|
+
# Summary
|
|
542
|
+
log.step "Project: #{result[:name]}"
|
|
543
|
+
log.step "Directory: #{result[:directory]}"
|
|
544
|
+
log.step "Template: #{result[:template]}"
|
|
545
|
+
log.step "TypeScript: #{result[:typescript] ? "Yes" : "No"}"
|
|
546
|
+
log.step "Features: #{result[:features].join(", ")}" unless result[:features].empty?
|
|
547
|
+
log.step "Color: #{color}"
|
|
548
|
+
log.step "Action: #{action}"
|
|
549
|
+
log.step "Config: #{config_path}"
|
|
550
|
+
log.step "Stack: #{stack.join(", ")}" unless stack.empty?
|
|
551
|
+
|
|
552
|
+
note <<~MSG, title: "Next steps"
|
|
553
|
+
cd #{result[:directory]}
|
|
554
|
+
#{result[:package_manager]} run dev
|
|
555
|
+
MSG
|
|
556
|
+
|
|
557
|
+
outro "Happy coding!"
|
|
558
|
+
end
|
|
559
|
+
# :nocov:
|
|
560
|
+
end
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
# Terminal cleanup on exit - show cursor if it was hidden
|
|
564
|
+
at_exit do
|
|
565
|
+
print "\e[?25h"
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
# Chain INT handler to restore cursor before passing to previous handler
|
|
569
|
+
previous_int_handler = trap("INT") do
|
|
570
|
+
print "\e[?25h"
|
|
571
|
+
case previous_int_handler
|
|
572
|
+
when Proc then previous_int_handler.call
|
|
573
|
+
when "DEFAULT", "SYSTEM_DEFAULT" then exit(130)
|
|
574
|
+
else exit(130)
|
|
575
|
+
end
|
|
576
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: clack
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Steve Whittaker
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Ruby port of Clack — effortlessly build beautiful command-line apps with
|
|
13
|
+
the modern, minimal aesthetic popularized by Vercel and Astro.
|
|
14
|
+
email:
|
|
15
|
+
- swhitt@gmail.com
|
|
16
|
+
executables:
|
|
17
|
+
- clack-demo
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- CHANGELOG.md
|
|
22
|
+
- LICENSE
|
|
23
|
+
- README.md
|
|
24
|
+
- exe/clack-demo
|
|
25
|
+
- lib/clack.rb
|
|
26
|
+
- lib/clack/box.rb
|
|
27
|
+
- lib/clack/colors.rb
|
|
28
|
+
- lib/clack/core/cursor.rb
|
|
29
|
+
- lib/clack/core/key_reader.rb
|
|
30
|
+
- lib/clack/core/options_helper.rb
|
|
31
|
+
- lib/clack/core/prompt.rb
|
|
32
|
+
- lib/clack/core/settings.rb
|
|
33
|
+
- lib/clack/core/text_input_helper.rb
|
|
34
|
+
- lib/clack/environment.rb
|
|
35
|
+
- lib/clack/group.rb
|
|
36
|
+
- lib/clack/log.rb
|
|
37
|
+
- lib/clack/note.rb
|
|
38
|
+
- lib/clack/prompts/autocomplete.rb
|
|
39
|
+
- lib/clack/prompts/autocomplete_multiselect.rb
|
|
40
|
+
- lib/clack/prompts/confirm.rb
|
|
41
|
+
- lib/clack/prompts/group_multiselect.rb
|
|
42
|
+
- lib/clack/prompts/multiselect.rb
|
|
43
|
+
- lib/clack/prompts/password.rb
|
|
44
|
+
- lib/clack/prompts/path.rb
|
|
45
|
+
- lib/clack/prompts/progress.rb
|
|
46
|
+
- lib/clack/prompts/select.rb
|
|
47
|
+
- lib/clack/prompts/select_key.rb
|
|
48
|
+
- lib/clack/prompts/spinner.rb
|
|
49
|
+
- lib/clack/prompts/tasks.rb
|
|
50
|
+
- lib/clack/prompts/text.rb
|
|
51
|
+
- lib/clack/stream.rb
|
|
52
|
+
- lib/clack/symbols.rb
|
|
53
|
+
- lib/clack/task_log.rb
|
|
54
|
+
- lib/clack/utils.rb
|
|
55
|
+
- lib/clack/validators.rb
|
|
56
|
+
- lib/clack/version.rb
|
|
57
|
+
homepage: https://github.com/swhitt/clackrb
|
|
58
|
+
licenses:
|
|
59
|
+
- MIT
|
|
60
|
+
metadata:
|
|
61
|
+
homepage_uri: https://github.com/swhitt/clackrb
|
|
62
|
+
source_code_uri: https://github.com/swhitt/clackrb
|
|
63
|
+
changelog_uri: https://github.com/swhitt/clackrb/blob/main/CHANGELOG.md
|
|
64
|
+
bug_tracker_uri: https://github.com/swhitt/clackrb/issues
|
|
65
|
+
rubygems_mfa_required: 'true'
|
|
66
|
+
rdoc_options: []
|
|
67
|
+
require_paths:
|
|
68
|
+
- lib
|
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: 3.2.0
|
|
74
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
75
|
+
requirements:
|
|
76
|
+
- - ">="
|
|
77
|
+
- !ruby/object:Gem::Version
|
|
78
|
+
version: '0'
|
|
79
|
+
requirements: []
|
|
80
|
+
rubygems_version: 3.6.9
|
|
81
|
+
specification_version: 4
|
|
82
|
+
summary: Beautiful, minimal CLI prompts
|
|
83
|
+
test_files: []
|