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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 82be7312cfdade424ca896c9ddb5c70e2bfadadc5f61c411a073d01d1daa73c5
|
|
4
|
+
data.tar.gz: 2322b602de289654d8ba79925b804027b49a2027eb1e76fe8708d315527706a6
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 2412c28b3f625313df7249154a2a7a72cd039e336f3ee2764130e7dc52a5f932358a1576bc793d8fa33b5862d60cf5dff319a5b6495546bd59cef24714c93b1f
|
|
7
|
+
data.tar.gz: dc26d51d23d264c510c913383e448b3b8552312d1a1a78db7cba71e69ace120cc8638f69adc615fb844652065520947286a46cf75657a6a8ecdbcb499ee58bfc
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Full prompt library: text, password, confirm, select, multiselect, autocomplete, path, select_key, spinner, progress, tasks, group_multiselect
|
|
7
|
+
- `Clack.group` for chaining prompts with shared results
|
|
8
|
+
- `Clack.stream` for streaming output from commands and iterables
|
|
9
|
+
- Vim-style navigation (`hjkl`) alongside arrow keys
|
|
10
|
+
- Zero runtime dependencies
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Ruby 3.2+ support
|
|
14
|
+
|
|
15
|
+
## [0.1.0]
|
|
16
|
+
|
|
17
|
+
Initial release.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Steve Whittaker
|
|
4
|
+
|
|
5
|
+
This project is a Ruby port of Clack (https://github.com/bombshell-dev/clack)
|
|
6
|
+
Originally created by Nate Moore and contributors, licensed under MIT.
|
|
7
|
+
|
|
8
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
9
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
10
|
+
in the Software without restriction, including without limitation the rights
|
|
11
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
12
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
13
|
+
furnished to do so, subject to the following conditions:
|
|
14
|
+
|
|
15
|
+
The above copyright notice and this permission notice shall be included in all
|
|
16
|
+
copies or substantial portions of the Software.
|
|
17
|
+
|
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
19
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
20
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
21
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
22
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
23
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
24
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
# Clack
|
|
2
|
+
|
|
3
|
+
**Effortlessly beautiful CLI prompts for Ruby.**
|
|
4
|
+
|
|
5
|
+
A faithful Ruby port of [@clack/prompts](https://github.com/bombshell-dev/clack).
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<img src="examples/demo.gif" width="640" alt="Clack demo">
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
## Why Clack?
|
|
12
|
+
|
|
13
|
+
- **Zero dependencies** - Pure Ruby, stdlib only
|
|
14
|
+
- **Beautiful by default** - Thoughtfully designed prompts that just look right
|
|
15
|
+
- **Vim-friendly** - Navigate with `hjkl` or arrow keys
|
|
16
|
+
- **Accessible** - Graceful ASCII fallbacks for limited terminals
|
|
17
|
+
- **Composable** - Group prompts together with `Clack.group`
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
# Gemfile
|
|
23
|
+
gem "clack"
|
|
24
|
+
|
|
25
|
+
# Or from GitHub
|
|
26
|
+
gem "clack", github: "swhitt/clackrb"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Or install directly
|
|
31
|
+
gem install clack
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
require "clack"
|
|
38
|
+
|
|
39
|
+
Clack.intro "project-setup"
|
|
40
|
+
|
|
41
|
+
result = Clack.group do |g|
|
|
42
|
+
g.prompt(:name) { Clack.text(message: "Project name?", placeholder: "my-app") }
|
|
43
|
+
g.prompt(:framework) do
|
|
44
|
+
Clack.select(
|
|
45
|
+
message: "Pick a framework",
|
|
46
|
+
options: [
|
|
47
|
+
{ value: "rails", label: "Ruby on Rails", hint: "recommended" },
|
|
48
|
+
{ value: "sinatra", label: "Sinatra" },
|
|
49
|
+
{ value: "roda", label: "Roda" }
|
|
50
|
+
]
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
g.prompt(:features) do
|
|
54
|
+
Clack.multiselect(
|
|
55
|
+
message: "Select features",
|
|
56
|
+
options: %w[api auth admin websockets]
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
if Clack.cancel?(result)
|
|
62
|
+
Clack.cancel("Setup cancelled")
|
|
63
|
+
exit 1
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
Clack.outro "You're all set!"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Demo
|
|
70
|
+
|
|
71
|
+
Try it yourself:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
ruby examples/full_demo.rb
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
<details>
|
|
78
|
+
<summary>Recording the demo GIF</summary>
|
|
79
|
+
|
|
80
|
+
Install [VHS](https://github.com/charmbracelet/vhs) and run:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
vhs examples/demo.tape
|
|
84
|
+
```
|
|
85
|
+
</details>
|
|
86
|
+
|
|
87
|
+
## Prompts
|
|
88
|
+
|
|
89
|
+
All prompts return the user's input, or `Clack::CANCEL` if they pressed Escape/Ctrl+C.
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
# Always check for cancellation
|
|
93
|
+
result = Clack.text(message: "Name?")
|
|
94
|
+
exit 1 if Clack.cancel?(result)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Text
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
name = Clack.text(
|
|
101
|
+
message: "What is your project named?",
|
|
102
|
+
placeholder: "my-project", # Shown when empty (dim)
|
|
103
|
+
default_value: "untitled", # Used if submitted empty
|
|
104
|
+
initial_value: "hello-world", # Pre-filled, editable
|
|
105
|
+
validate: ->(v) { "Required!" if v.empty? }
|
|
106
|
+
)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Password
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
secret = Clack.password(
|
|
113
|
+
message: "Enter your API key",
|
|
114
|
+
mask: "*" # Default: "▪"
|
|
115
|
+
)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Confirm
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
proceed = Clack.confirm(
|
|
122
|
+
message: "Deploy to production?",
|
|
123
|
+
active: "Yes, ship it!",
|
|
124
|
+
inactive: "No, abort",
|
|
125
|
+
initial_value: false
|
|
126
|
+
)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Select
|
|
130
|
+
|
|
131
|
+
Single selection with keyboard navigation.
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
db = Clack.select(
|
|
135
|
+
message: "Choose a database",
|
|
136
|
+
options: [
|
|
137
|
+
{ value: "pg", label: "PostgreSQL", hint: "recommended" },
|
|
138
|
+
{ value: "mysql", label: "MySQL" },
|
|
139
|
+
{ value: "sqlite", label: "SQLite", disabled: true }
|
|
140
|
+
],
|
|
141
|
+
initial_value: "pg",
|
|
142
|
+
max_items: 5 # Enable scrolling
|
|
143
|
+
)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Multiselect
|
|
147
|
+
|
|
148
|
+
Multiple selections with toggle controls.
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
features = Clack.multiselect(
|
|
152
|
+
message: "Select features to install",
|
|
153
|
+
options: [
|
|
154
|
+
{ value: "api", label: "API Mode" },
|
|
155
|
+
{ value: "auth", label: "Authentication" },
|
|
156
|
+
{ value: "jobs", label: "Background Jobs" }
|
|
157
|
+
],
|
|
158
|
+
initial_values: ["api"],
|
|
159
|
+
required: true, # Must select at least one
|
|
160
|
+
max_items: 5 # Enable scrolling
|
|
161
|
+
)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Shortcuts:** `Space` toggle | `a` all | `i` invert
|
|
165
|
+
|
|
166
|
+
### Autocomplete
|
|
167
|
+
|
|
168
|
+
Type to filter from a list of options.
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
color = Clack.autocomplete(
|
|
172
|
+
message: "Pick a color",
|
|
173
|
+
options: %w[red orange yellow green blue indigo violet],
|
|
174
|
+
placeholder: "Type to search..."
|
|
175
|
+
)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Autocomplete Multiselect
|
|
179
|
+
|
|
180
|
+
Type to filter with multi-selection support.
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
colors = Clack.autocomplete_multiselect(
|
|
184
|
+
message: "Pick colors",
|
|
185
|
+
options: %w[red orange yellow green blue indigo violet],
|
|
186
|
+
placeholder: "Type to filter...",
|
|
187
|
+
required: true, # At least one selection required
|
|
188
|
+
initial_values: ["red"] # Pre-selected values
|
|
189
|
+
)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**Shortcuts:** `Space` toggle | `a` toggle all | `i` invert | `Enter` confirm
|
|
193
|
+
|
|
194
|
+
### Path
|
|
195
|
+
|
|
196
|
+
File/directory path selector with filesystem navigation.
|
|
197
|
+
|
|
198
|
+
```ruby
|
|
199
|
+
project_dir = Clack.path(
|
|
200
|
+
message: "Where should we create your project?",
|
|
201
|
+
only_directories: true, # Only show directories
|
|
202
|
+
root: "." # Starting directory
|
|
203
|
+
)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**Navigation:** Type to filter | `Tab` to autocomplete | `↑↓` to select
|
|
207
|
+
|
|
208
|
+
### Select Key
|
|
209
|
+
|
|
210
|
+
Quick selection using keyboard shortcuts.
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
action = Clack.select_key(
|
|
214
|
+
message: "What would you like to do?",
|
|
215
|
+
options: [
|
|
216
|
+
{ value: "create", label: "Create new project", key: "c" },
|
|
217
|
+
{ value: "open", label: "Open existing", key: "o" },
|
|
218
|
+
{ value: "quit", label: "Quit", key: "q" }
|
|
219
|
+
]
|
|
220
|
+
)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Spinner
|
|
224
|
+
|
|
225
|
+
Non-blocking animated indicator for async work.
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
spinner = Clack.spinner
|
|
229
|
+
spinner.start("Installing dependencies...")
|
|
230
|
+
|
|
231
|
+
# Do your work...
|
|
232
|
+
sleep 2
|
|
233
|
+
|
|
234
|
+
spinner.stop("Dependencies installed!")
|
|
235
|
+
# Or: spinner.error("Installation failed")
|
|
236
|
+
# Or: spinner.cancel("Cancelled")
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Progress
|
|
240
|
+
|
|
241
|
+
Visual progress bar for measurable operations.
|
|
242
|
+
|
|
243
|
+
```ruby
|
|
244
|
+
progress = Clack.progress(total: 100, message: "Downloading...")
|
|
245
|
+
progress.start
|
|
246
|
+
|
|
247
|
+
files.each_with_index do |file, i|
|
|
248
|
+
download(file)
|
|
249
|
+
progress.update(i + 1)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
progress.stop("Download complete!")
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Tasks
|
|
256
|
+
|
|
257
|
+
Run multiple tasks with status indicators.
|
|
258
|
+
|
|
259
|
+
```ruby
|
|
260
|
+
results = Clack.tasks(tasks: [
|
|
261
|
+
{ title: "Checking dependencies", task: -> { check_deps } },
|
|
262
|
+
{ title: "Building project", task: -> { build } },
|
|
263
|
+
{ title: "Running tests", task: -> { run_tests } }
|
|
264
|
+
])
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Group Multiselect
|
|
268
|
+
|
|
269
|
+
Multiselect with options organized into groups.
|
|
270
|
+
|
|
271
|
+
```ruby
|
|
272
|
+
features = Clack.group_multiselect(
|
|
273
|
+
message: "Select features",
|
|
274
|
+
options: [
|
|
275
|
+
{
|
|
276
|
+
label: "Frontend",
|
|
277
|
+
options: [
|
|
278
|
+
{ value: "hotwire", label: "Hotwire" },
|
|
279
|
+
{ value: "stimulus", label: "Stimulus" }
|
|
280
|
+
]
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
label: "Background",
|
|
284
|
+
options: [
|
|
285
|
+
{ value: "sidekiq", label: "Sidekiq" },
|
|
286
|
+
{ value: "solid_queue", label: "Solid Queue" }
|
|
287
|
+
]
|
|
288
|
+
}
|
|
289
|
+
]
|
|
290
|
+
)
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Prompt Groups
|
|
294
|
+
|
|
295
|
+
Chain multiple prompts and collect results in a hash. Cancellation is handled automatically.
|
|
296
|
+
|
|
297
|
+
```ruby
|
|
298
|
+
result = Clack.group do |g|
|
|
299
|
+
g.prompt(:name) { Clack.text(message: "Your name?") }
|
|
300
|
+
g.prompt(:email) { Clack.text(message: "Your email?") }
|
|
301
|
+
g.prompt(:confirm) { |r| Clack.confirm(message: "Create account for #{r[:email]}?") }
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
return if Clack.cancel?(result)
|
|
305
|
+
|
|
306
|
+
puts "Welcome, #{result[:name]}!"
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Handle cancellation with a callback:
|
|
310
|
+
|
|
311
|
+
```ruby
|
|
312
|
+
Clack.group(on_cancel: ->(r) { cleanup(r) }) do |g|
|
|
313
|
+
# prompts...
|
|
314
|
+
end
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Logging
|
|
318
|
+
|
|
319
|
+
Beautiful, consistent log messages.
|
|
320
|
+
|
|
321
|
+
```ruby
|
|
322
|
+
Clack.log.info("Starting build...")
|
|
323
|
+
Clack.log.success("Build completed!")
|
|
324
|
+
Clack.log.warn("Cache is stale")
|
|
325
|
+
Clack.log.error("Build failed")
|
|
326
|
+
Clack.log.step("Running migrations")
|
|
327
|
+
Clack.log.message("Custom message")
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Stream
|
|
331
|
+
|
|
332
|
+
Stream output from iterables, enumerables, or shell commands:
|
|
333
|
+
|
|
334
|
+
```ruby
|
|
335
|
+
# Stream from an array or enumerable
|
|
336
|
+
Clack.stream.info(["Line 1", "Line 2", "Line 3"])
|
|
337
|
+
Clack.stream.step(["Step 1", "Step 2", "Step 3"])
|
|
338
|
+
|
|
339
|
+
# Stream from a shell command (returns true/false for success)
|
|
340
|
+
success = Clack.stream.command("npm install", type: :info)
|
|
341
|
+
|
|
342
|
+
# Stream from any IO or StringIO
|
|
343
|
+
Clack.stream.success(io_stream)
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## Note
|
|
347
|
+
|
|
348
|
+
Display important information in a box.
|
|
349
|
+
|
|
350
|
+
```ruby
|
|
351
|
+
Clack.note(<<~MSG, title: "Next Steps")
|
|
352
|
+
cd my-project
|
|
353
|
+
bundle install
|
|
354
|
+
bin/rails server
|
|
355
|
+
MSG
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Box
|
|
359
|
+
|
|
360
|
+
Render a customizable bordered box.
|
|
361
|
+
|
|
362
|
+
```ruby
|
|
363
|
+
Clack.box("Hello, World!", title: "Greeting")
|
|
364
|
+
|
|
365
|
+
# With options
|
|
366
|
+
Clack.box(
|
|
367
|
+
"Centered content",
|
|
368
|
+
title: "My Box",
|
|
369
|
+
content_align: :center, # :left, :center, :right
|
|
370
|
+
title_align: :center,
|
|
371
|
+
width: 40, # or :auto to fit content
|
|
372
|
+
rounded: true # rounded or square corners
|
|
373
|
+
)
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### Task Log
|
|
377
|
+
|
|
378
|
+
Streaming log that clears on success and shows full output on failure. Useful for build output.
|
|
379
|
+
|
|
380
|
+
```ruby
|
|
381
|
+
tl = Clack.task_log(title: "Building...", limit: 10)
|
|
382
|
+
|
|
383
|
+
tl.message("Compiling file 1...")
|
|
384
|
+
tl.message("Compiling file 2...")
|
|
385
|
+
|
|
386
|
+
# On success: clears the log
|
|
387
|
+
tl.success("Build complete!")
|
|
388
|
+
|
|
389
|
+
# On error: keeps the log visible
|
|
390
|
+
# tl.error("Build failed!")
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
## Session Markers
|
|
394
|
+
|
|
395
|
+
```ruby
|
|
396
|
+
Clack.intro("my-cli v1.0") # ┌ my-cli v1.0
|
|
397
|
+
# ... your prompts ...
|
|
398
|
+
Clack.outro("Done!") # └ Done!
|
|
399
|
+
|
|
400
|
+
# Or on error:
|
|
401
|
+
Clack.cancel("Aborted") # └ Aborted (red)
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
## Requirements
|
|
405
|
+
|
|
406
|
+
- Ruby 3.2+
|
|
407
|
+
- No runtime dependencies
|
|
408
|
+
|
|
409
|
+
## Development
|
|
410
|
+
|
|
411
|
+
```bash
|
|
412
|
+
bundle install
|
|
413
|
+
bundle exec rake # Lint + tests
|
|
414
|
+
bundle exec rake spec # Tests only
|
|
415
|
+
COVERAGE=true bundle exec rake spec # With coverage
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
## Credits
|
|
419
|
+
|
|
420
|
+
This is a Ruby port of [Clack](https://github.com/bombshell-dev/clack), created by [Nate Moore](https://github.com/natemoo-re) and the [Astro](https://astro.build) team.
|
|
421
|
+
|
|
422
|
+
## License
|
|
423
|
+
|
|
424
|
+
MIT - See [LICENSE](LICENSE)
|
data/exe/clack-demo
ADDED
data/lib/clack/box.rb
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clack
|
|
4
|
+
# Renders a box with optional title around content
|
|
5
|
+
# Supports alignment, padding, and rounded/square corners
|
|
6
|
+
module Box
|
|
7
|
+
class << self
|
|
8
|
+
# @param message [String] Content to display in the box
|
|
9
|
+
# @param title [String] Optional title for the box
|
|
10
|
+
# @param content_align [:left, :center, :right] Content alignment
|
|
11
|
+
# @param title_align [:left, :center, :right] Title alignment
|
|
12
|
+
# @param width [Integer, :auto] Box width (auto fits to content)
|
|
13
|
+
# @param title_padding [Integer] Padding around title
|
|
14
|
+
# @param content_padding [Integer] Padding around content
|
|
15
|
+
# @param rounded [Boolean] Use rounded corners (default: true)
|
|
16
|
+
# @param format_border [Proc] Optional proc to format border characters
|
|
17
|
+
# @param output [IO] Output stream
|
|
18
|
+
def render(
|
|
19
|
+
message = "",
|
|
20
|
+
title: "",
|
|
21
|
+
content_align: :left,
|
|
22
|
+
title_align: :left,
|
|
23
|
+
width: :auto,
|
|
24
|
+
title_padding: 1,
|
|
25
|
+
content_padding: 2,
|
|
26
|
+
rounded: true,
|
|
27
|
+
format_border: nil,
|
|
28
|
+
output: $stdout
|
|
29
|
+
)
|
|
30
|
+
ctx = build_context(message, title, title_padding, content_padding, width, rounded, format_border)
|
|
31
|
+
output.puts build_top_border(ctx[:display_title], ctx[:inner_width], title_padding, title_align, ctx[:symbols], ctx[:h_symbol])
|
|
32
|
+
render_content_lines(output, ctx, content_align, content_padding)
|
|
33
|
+
output.puts "#{ctx[:symbols][2]}#{ctx[:h_symbol] * ctx[:inner_width]}#{ctx[:symbols][3]}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def build_context(message, title, title_padding, content_padding, width, rounded, format_border)
|
|
39
|
+
format_border ||= ->(text) { Colors.gray(text) }
|
|
40
|
+
symbols = corner_symbols(rounded).map(&format_border)
|
|
41
|
+
lines = message.to_s.lines.map(&:chomp)
|
|
42
|
+
box_width = calculate_width(lines, title.length, title_padding, content_padding, width)
|
|
43
|
+
inner_width = box_width - 2
|
|
44
|
+
max_title_len = inner_width - (title_padding * 2)
|
|
45
|
+
display_title = (title.length > max_title_len) ? "#{title[0, max_title_len - 3]}..." : title
|
|
46
|
+
|
|
47
|
+
{
|
|
48
|
+
symbols: symbols,
|
|
49
|
+
h_symbol: format_border.call(Symbols::S_BAR_H),
|
|
50
|
+
v_symbol: format_border.call(Symbols::S_BAR),
|
|
51
|
+
lines: lines,
|
|
52
|
+
inner_width: inner_width,
|
|
53
|
+
display_title: display_title
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def render_content_lines(output, ctx, content_align, content_padding)
|
|
58
|
+
ctx[:lines].each do |line|
|
|
59
|
+
left_pad, right_pad = padding_for_line(line.length, ctx[:inner_width], content_padding, content_align)
|
|
60
|
+
output.puts "#{ctx[:v_symbol]}#{" " * left_pad}#{line}#{" " * right_pad}#{ctx[:v_symbol]}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def corner_symbols(rounded)
|
|
65
|
+
if rounded
|
|
66
|
+
[
|
|
67
|
+
Symbols::S_CORNER_TOP_LEFT,
|
|
68
|
+
Symbols::S_CORNER_TOP_RIGHT,
|
|
69
|
+
Symbols::S_CORNER_BOTTOM_LEFT,
|
|
70
|
+
Symbols::S_CORNER_BOTTOM_RIGHT
|
|
71
|
+
]
|
|
72
|
+
else
|
|
73
|
+
[
|
|
74
|
+
Symbols::S_BAR_START,
|
|
75
|
+
Symbols::S_BAR_START_RIGHT,
|
|
76
|
+
Symbols::S_BAR_END,
|
|
77
|
+
Symbols::S_BAR_END_RIGHT
|
|
78
|
+
]
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def calculate_width(lines, title_len, title_padding, content_padding, width)
|
|
83
|
+
return width + 2 if width.is_a?(Integer) # Add 2 for borders
|
|
84
|
+
|
|
85
|
+
# Auto width: fit to content
|
|
86
|
+
max_line = lines.map(&:length).max || 0
|
|
87
|
+
title_with_padding = title_len + (title_padding * 2)
|
|
88
|
+
content_with_padding = max_line + (content_padding * 2)
|
|
89
|
+
|
|
90
|
+
[title_with_padding, content_with_padding].max + 2
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def build_top_border(title, inner_width, title_padding, title_align, symbols, h_symbol)
|
|
94
|
+
if title.empty?
|
|
95
|
+
"#{symbols[0]}#{h_symbol * inner_width}#{symbols[1]}"
|
|
96
|
+
else
|
|
97
|
+
left_pad, right_pad = padding_for_line(title.length, inner_width, title_padding, title_align)
|
|
98
|
+
"#{symbols[0]}#{h_symbol * left_pad}#{title}#{h_symbol * right_pad}#{symbols[1]}"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def padding_for_line(line_length, inner_width, padding, align)
|
|
103
|
+
case align
|
|
104
|
+
when :center
|
|
105
|
+
left = (inner_width - line_length) / 2
|
|
106
|
+
right = inner_width - left - line_length
|
|
107
|
+
[left, right]
|
|
108
|
+
when :right
|
|
109
|
+
left = inner_width - line_length - padding
|
|
110
|
+
right = padding
|
|
111
|
+
[[left, 0].max, right]
|
|
112
|
+
else # :left
|
|
113
|
+
left = padding
|
|
114
|
+
right = inner_width - left - line_length
|
|
115
|
+
[left, [right, 0].max]
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
data/lib/clack/colors.rb
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clack
|
|
4
|
+
# ANSI color codes for terminal output styling.
|
|
5
|
+
# Colors are automatically disabled when:
|
|
6
|
+
# - Output is not a TTY (piped/redirected)
|
|
7
|
+
# - NO_COLOR environment variable is set
|
|
8
|
+
# - FORCE_COLOR environment variable forces colors on
|
|
9
|
+
module Colors
|
|
10
|
+
class << self
|
|
11
|
+
def enabled?
|
|
12
|
+
return true if ENV["FORCE_COLOR"] && ENV["FORCE_COLOR"] != "0"
|
|
13
|
+
return false if ENV["NO_COLOR"]
|
|
14
|
+
|
|
15
|
+
$stdout.tty?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Foreground colors (standard)
|
|
19
|
+
def gray(text) = wrap(text, "90")
|
|
20
|
+
def cyan(text) = wrap(text, "36")
|
|
21
|
+
def green(text) = wrap(text, "32")
|
|
22
|
+
def yellow(text) = wrap(text, "33")
|
|
23
|
+
def red(text) = wrap(text, "31")
|
|
24
|
+
def blue(text) = wrap(text, "34")
|
|
25
|
+
def magenta(text) = wrap(text, "35")
|
|
26
|
+
def white(text) = wrap(text, "37")
|
|
27
|
+
|
|
28
|
+
# Text styles
|
|
29
|
+
def dim(text) = wrap(text, "2")
|
|
30
|
+
def bold(text) = wrap(text, "1")
|
|
31
|
+
def italic(text) = wrap(text, "3")
|
|
32
|
+
def underline(text) = wrap(text, "4")
|
|
33
|
+
def inverse(text) = wrap(text, "7")
|
|
34
|
+
def strikethrough(text) = wrap(text, "9")
|
|
35
|
+
def hidden(text) = wrap(text, "8")
|
|
36
|
+
|
|
37
|
+
# Bright/vivid foreground colors (higher contrast)
|
|
38
|
+
def bright_cyan(text) = wrap(text, "96")
|
|
39
|
+
def bright_green(text) = wrap(text, "92")
|
|
40
|
+
def bright_yellow(text) = wrap(text, "93")
|
|
41
|
+
def bright_red(text) = wrap(text, "91")
|
|
42
|
+
def bright_blue(text) = wrap(text, "94")
|
|
43
|
+
def bright_magenta(text) = wrap(text, "95")
|
|
44
|
+
def bright_white(text) = wrap(text, "97")
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def wrap(text, code)
|
|
49
|
+
return text.to_s unless enabled?
|
|
50
|
+
|
|
51
|
+
"\e[#{code}m#{text}\e[0m"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|