clack 0.1.4 → 0.2.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 +4 -4
- data/CHANGELOG.md +26 -1
- data/README.md +108 -8
- data/examples/advanced_prompts.rb +63 -0
- data/examples/basic.rb +15 -0
- data/examples/create_app.rb +86 -0
- data/examples/date_demo.rb +40 -0
- data/examples/demo.rb +179 -0
- data/examples/full_demo.rb +84 -0
- data/examples/group_demo.rb +79 -0
- data/examples/images/confirm_example.rb +12 -0
- data/examples/images/multiselect_example.rb +15 -0
- data/examples/images/password_example.rb +10 -0
- data/examples/images/select_example.rb +15 -0
- data/examples/images/spinner_example.rb +11 -0
- data/examples/images/text_example.rb +11 -0
- data/examples/spinner_demo.rb +38 -0
- data/examples/tasks_demo.rb +59 -0
- data/examples/validation.rb +73 -0
- data/lib/clack/colors.rb +97 -3
- data/lib/clack/core/cursor.rb +1 -0
- data/lib/clack/core/key_reader.rb +5 -0
- data/lib/clack/core/prompt.rb +52 -8
- data/lib/clack/core/settings.rb +4 -0
- data/lib/clack/core/text_input_helper.rb +4 -0
- data/lib/clack/log.rb +51 -0
- data/lib/clack/note.rb +7 -0
- data/lib/clack/prompts/autocomplete.rb +29 -11
- data/lib/clack/prompts/autocomplete_multiselect.rb +5 -6
- data/lib/clack/prompts/date.rb +280 -0
- data/lib/clack/prompts/group_multiselect.rb +46 -18
- data/lib/clack/prompts/multiline_text.rb +8 -9
- data/lib/clack/prompts/multiselect.rb +3 -5
- data/lib/clack/prompts/password.rb +5 -10
- data/lib/clack/prompts/path.rb +2 -2
- data/lib/clack/prompts/progress.rb +2 -6
- data/lib/clack/prompts/select.rb +2 -6
- data/lib/clack/prompts/select_key.rb +5 -7
- data/lib/clack/prompts/spinner.rb +2 -6
- data/lib/clack/prompts/tasks.rb +50 -9
- data/lib/clack/prompts/text.rb +4 -3
- data/lib/clack/stream.rb +32 -3
- data/lib/clack/symbols.rb +25 -0
- data/lib/clack/task_log.rb +3 -5
- data/lib/clack/transformers.rb +8 -7
- data/lib/clack/validators.rb +33 -2
- data/lib/clack/version.rb +2 -1
- data/lib/clack.rb +72 -213
- metadata +18 -1
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Demonstrates Clack.group and group_multiselect
|
|
5
|
+
# Run with: ruby examples/group_demo.rb
|
|
6
|
+
|
|
7
|
+
require_relative "../lib/clack" # Or: require "clack" if installed as a gem
|
|
8
|
+
|
|
9
|
+
Clack.intro "group-demo"
|
|
10
|
+
|
|
11
|
+
# Prompt group with on_cancel handler
|
|
12
|
+
result = Clack.group(
|
|
13
|
+
on_cancel: ->(partial) {
|
|
14
|
+
Clack.cancel "Cancelled (collected: #{partial.keys.select { |k| partial[k] != :cancelled }.join(", ")})"
|
|
15
|
+
exit 0
|
|
16
|
+
}
|
|
17
|
+
) do |g|
|
|
18
|
+
g.prompt(:name) { Clack.text(message: "Project name:", placeholder: "my-project") }
|
|
19
|
+
|
|
20
|
+
g.prompt(:visibility) do |results|
|
|
21
|
+
Clack.select(
|
|
22
|
+
message: "Visibility for #{results[:name]}?",
|
|
23
|
+
options: [
|
|
24
|
+
{value: "public", label: "Public", hint: "visible to everyone"},
|
|
25
|
+
{value: "private", label: "Private", hint: "invite only"}
|
|
26
|
+
]
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
g.prompt(:confirm) do |results|
|
|
31
|
+
Clack.confirm(message: "Create #{results[:visibility]} project '#{results[:name]}'?")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
unless result[:confirm]
|
|
36
|
+
Clack.outro "Project creation skipped."
|
|
37
|
+
exit 0
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
Clack.log.success "Project: #{result[:name]} (#{result[:visibility]})"
|
|
41
|
+
|
|
42
|
+
# Group multiselect with selectable group headers and spacing
|
|
43
|
+
stack = Clack.group_multiselect(
|
|
44
|
+
message: "Select stack components:",
|
|
45
|
+
selectable_groups: true,
|
|
46
|
+
group_spacing: 1,
|
|
47
|
+
required: true,
|
|
48
|
+
options: [
|
|
49
|
+
{
|
|
50
|
+
label: "Frontend",
|
|
51
|
+
options: [
|
|
52
|
+
{value: "react", label: "React"},
|
|
53
|
+
{value: "tailwind", label: "Tailwind CSS"},
|
|
54
|
+
{value: "typescript", label: "TypeScript"}
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
label: "Backend",
|
|
59
|
+
options: [
|
|
60
|
+
{value: "rails", label: "Ruby on Rails"},
|
|
61
|
+
{value: "sidekiq", label: "Sidekiq"},
|
|
62
|
+
{value: "graphql", label: "GraphQL"}
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
label: "Infrastructure",
|
|
67
|
+
options: [
|
|
68
|
+
{value: "docker", label: "Docker"},
|
|
69
|
+
{value: "k8s", label: "Kubernetes"},
|
|
70
|
+
{value: "terraform", label: "Terraform"}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
)
|
|
75
|
+
exit 0 if Clack.cancel?(stack)
|
|
76
|
+
|
|
77
|
+
Clack.log.step "Stack: #{stack.join(", ")}"
|
|
78
|
+
|
|
79
|
+
Clack.outro "Done!"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Run with: ruby examples/images/confirm_example.rb
|
|
5
|
+
|
|
6
|
+
require_relative "../../lib/clack" # Or: require "clack" if installed as a gem
|
|
7
|
+
|
|
8
|
+
Clack.confirm(
|
|
9
|
+
message: "Deploy to production?",
|
|
10
|
+
active: "Yes, ship it!",
|
|
11
|
+
inactive: "No, abort"
|
|
12
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Run with: ruby examples/images/multiselect_example.rb
|
|
5
|
+
|
|
6
|
+
require_relative "../../lib/clack" # Or: require "clack" if installed as a gem
|
|
7
|
+
|
|
8
|
+
Clack.multiselect(
|
|
9
|
+
message: "Select features to install",
|
|
10
|
+
options: [
|
|
11
|
+
{value: "api", label: "API Mode"},
|
|
12
|
+
{value: "auth", label: "Authentication"},
|
|
13
|
+
{value: "jobs", label: "Background Jobs"}
|
|
14
|
+
]
|
|
15
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Run with: ruby examples/images/select_example.rb
|
|
5
|
+
|
|
6
|
+
require_relative "../../lib/clack" # Or: require "clack" if installed as a gem
|
|
7
|
+
|
|
8
|
+
Clack.select(
|
|
9
|
+
message: "Choose a database",
|
|
10
|
+
options: [
|
|
11
|
+
{value: "pg", label: "PostgreSQL", hint: "recommended"},
|
|
12
|
+
{value: "mysql", label: "MySQL"},
|
|
13
|
+
{value: "sqlite", label: "SQLite"}
|
|
14
|
+
]
|
|
15
|
+
)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Run with: ruby examples/images/spinner_example.rb
|
|
5
|
+
|
|
6
|
+
require_relative "../../lib/clack" # Or: require "clack" if installed as a gem
|
|
7
|
+
|
|
8
|
+
spinner = Clack.spinner
|
|
9
|
+
spinner.start("Installing dependencies...")
|
|
10
|
+
sleep 1.5
|
|
11
|
+
spinner.stop("Dependencies installed!")
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Run with: ruby examples/images/text_example.rb
|
|
5
|
+
|
|
6
|
+
require_relative "../../lib/clack" # Or: require "clack" if installed as a gem
|
|
7
|
+
|
|
8
|
+
Clack.text(
|
|
9
|
+
message: "What is your project named?",
|
|
10
|
+
placeholder: "my-project"
|
|
11
|
+
)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Run with: ruby examples/spinner_demo.rb
|
|
5
|
+
|
|
6
|
+
require_relative "../lib/clack" # Or: require "clack" if installed as a gem
|
|
7
|
+
|
|
8
|
+
Clack.intro "spinner-demo"
|
|
9
|
+
|
|
10
|
+
# Success state
|
|
11
|
+
s = Clack.spinner
|
|
12
|
+
s.start "Installing dependencies..."
|
|
13
|
+
sleep 2
|
|
14
|
+
s.stop "Dependencies installed!"
|
|
15
|
+
|
|
16
|
+
# Update message mid-spin
|
|
17
|
+
s = Clack.spinner
|
|
18
|
+
s.start "Building project..."
|
|
19
|
+
sleep 1
|
|
20
|
+
s.message "Compiling assets..."
|
|
21
|
+
sleep 1
|
|
22
|
+
s.message "Optimizing bundles..."
|
|
23
|
+
sleep 1
|
|
24
|
+
s.stop "Build complete!"
|
|
25
|
+
|
|
26
|
+
# Error state
|
|
27
|
+
s = Clack.spinner
|
|
28
|
+
s.start "Running tests..."
|
|
29
|
+
sleep 1.5
|
|
30
|
+
s.error "Tests failed!"
|
|
31
|
+
|
|
32
|
+
# Cancel state
|
|
33
|
+
s = Clack.spinner
|
|
34
|
+
s.start "Deploying to production..."
|
|
35
|
+
sleep 1
|
|
36
|
+
s.cancel "Deployment cancelled"
|
|
37
|
+
|
|
38
|
+
Clack.outro "Demo complete!"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Demonstrates Clack.tasks and Clack.task_log
|
|
5
|
+
# Run with: ruby examples/tasks_demo.rb
|
|
6
|
+
|
|
7
|
+
require_relative "../lib/clack" # Or: require "clack" if installed as a gem
|
|
8
|
+
|
|
9
|
+
Clack.intro "tasks-demo"
|
|
10
|
+
|
|
11
|
+
# Sequential tasks with spinner (message callback updates the spinner text)
|
|
12
|
+
Clack.tasks(tasks: [
|
|
13
|
+
{
|
|
14
|
+
title: "Checking dependencies",
|
|
15
|
+
task: -> {
|
|
16
|
+
sleep 1
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
title: "Installing packages",
|
|
21
|
+
task: ->(message) {
|
|
22
|
+
sleep 0.5
|
|
23
|
+
message.call("Installing packages... (1/3) core")
|
|
24
|
+
sleep 0.5
|
|
25
|
+
message.call("Installing packages... (2/3) dev tools")
|
|
26
|
+
sleep 0.5
|
|
27
|
+
message.call("Installing packages... (3/3) plugins")
|
|
28
|
+
sleep 0.5
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
title: "Compiling assets",
|
|
33
|
+
task: -> {
|
|
34
|
+
sleep 1.5
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
# Task log with streaming output
|
|
40
|
+
tl = Clack.task_log(title: "Running test suite", limit: 5)
|
|
41
|
+
|
|
42
|
+
test_files = %w[
|
|
43
|
+
models/user_test.rb
|
|
44
|
+
models/post_test.rb
|
|
45
|
+
controllers/auth_test.rb
|
|
46
|
+
controllers/api_test.rb
|
|
47
|
+
helpers/format_test.rb
|
|
48
|
+
integration/signup_test.rb
|
|
49
|
+
integration/login_test.rb
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
test_files.each do |file|
|
|
53
|
+
tl.message "Running #{file}..."
|
|
54
|
+
sleep 0.3
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
tl.success "All #{test_files.length} test files passed!"
|
|
58
|
+
|
|
59
|
+
Clack.outro "Done!"
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Run with: ruby examples/validation.rb
|
|
5
|
+
|
|
6
|
+
require_relative "../lib/clack" # Or: require "clack" if installed as a gem
|
|
7
|
+
|
|
8
|
+
Clack.intro "validation-demo"
|
|
9
|
+
|
|
10
|
+
# Required field
|
|
11
|
+
name = Clack.text(
|
|
12
|
+
message: "Username (required)",
|
|
13
|
+
validate: ->(v) { "Username is required" if v.to_s.strip.empty? }
|
|
14
|
+
)
|
|
15
|
+
exit 0 if Clack.cancel?(name)
|
|
16
|
+
|
|
17
|
+
# Length validation
|
|
18
|
+
password = Clack.password(
|
|
19
|
+
message: "Password (min 8 chars)",
|
|
20
|
+
validate: ->(v) { "Password must be at least 8 characters" if v.to_s.length < 8 }
|
|
21
|
+
)
|
|
22
|
+
exit 0 if Clack.cancel?(password)
|
|
23
|
+
|
|
24
|
+
# Format validation
|
|
25
|
+
email = Clack.text(
|
|
26
|
+
message: "Email address",
|
|
27
|
+
validate: lambda { |v|
|
|
28
|
+
return "Email is required" if v.to_s.strip.empty?
|
|
29
|
+
return "Invalid email format" unless v.to_s.include?("@")
|
|
30
|
+
|
|
31
|
+
nil
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
exit 0 if Clack.cancel?(email)
|
|
35
|
+
|
|
36
|
+
# Phone number with validation and custom transform
|
|
37
|
+
phone = Clack.text(
|
|
38
|
+
message: "Phone number",
|
|
39
|
+
validate: ->(v) { "Enter 10 digits" unless v.gsub(/\D/, "").length == 10 },
|
|
40
|
+
transform: ->(v) {
|
|
41
|
+
digits = v.gsub(/\D/, "")
|
|
42
|
+
"(#{digits[0, 3]}) #{digits[3, 3]}-#{digits[6, 4]}"
|
|
43
|
+
}
|
|
44
|
+
)
|
|
45
|
+
exit 0 if Clack.cancel?(phone)
|
|
46
|
+
|
|
47
|
+
# Username with transform (symbol shortcuts + custom)
|
|
48
|
+
handle = Clack.text(
|
|
49
|
+
message: "Twitter handle",
|
|
50
|
+
transform: Clack::Transformers.chain(:strip, :downcase, ->(v) { v.delete_prefix("@") })
|
|
51
|
+
)
|
|
52
|
+
exit 0 if Clack.cancel?(handle)
|
|
53
|
+
|
|
54
|
+
# Multiselect required
|
|
55
|
+
features = Clack.multiselect(
|
|
56
|
+
message: "Select at least one feature",
|
|
57
|
+
options: [
|
|
58
|
+
{value: "a", label: "Feature A"},
|
|
59
|
+
{value: "b", label: "Feature B"},
|
|
60
|
+
{value: "c", label: "Feature C"}
|
|
61
|
+
],
|
|
62
|
+
required: true # Built-in validation
|
|
63
|
+
)
|
|
64
|
+
exit 0 if Clack.cancel?(features)
|
|
65
|
+
|
|
66
|
+
Clack.log.success "All validations passed!"
|
|
67
|
+
Clack.log.info "Username: #{name}"
|
|
68
|
+
Clack.log.info "Email: #{email}"
|
|
69
|
+
Clack.log.info "Phone: #{phone}"
|
|
70
|
+
Clack.log.info "Handle: @#{handle}"
|
|
71
|
+
Clack.log.info "Features: #{features.join(", ")}"
|
|
72
|
+
|
|
73
|
+
Clack.outro "Done!"
|
data/lib/clack/colors.rb
CHANGED
|
@@ -15,34 +15,128 @@ module Clack
|
|
|
15
15
|
$stdout.tty?
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
# Foreground
|
|
18
|
+
# @!group Foreground Colors (standard)
|
|
19
|
+
|
|
20
|
+
# Apply gray foreground color (ANSI 90).
|
|
21
|
+
# @param text [#to_s] text to colorize
|
|
22
|
+
# @return [String] ANSI-wrapped text
|
|
19
23
|
def gray(text) = wrap(text, "90")
|
|
24
|
+
|
|
25
|
+
# Apply cyan foreground color (ANSI 36).
|
|
26
|
+
# @param text [#to_s] text to colorize
|
|
27
|
+
# @return [String] ANSI-wrapped text
|
|
20
28
|
def cyan(text) = wrap(text, "36")
|
|
29
|
+
|
|
30
|
+
# Apply green foreground color (ANSI 32).
|
|
31
|
+
# @param text [#to_s] text to colorize
|
|
32
|
+
# @return [String] ANSI-wrapped text
|
|
21
33
|
def green(text) = wrap(text, "32")
|
|
34
|
+
|
|
35
|
+
# Apply yellow foreground color (ANSI 33).
|
|
36
|
+
# @param text [#to_s] text to colorize
|
|
37
|
+
# @return [String] ANSI-wrapped text
|
|
22
38
|
def yellow(text) = wrap(text, "33")
|
|
39
|
+
|
|
40
|
+
# Apply red foreground color (ANSI 31).
|
|
41
|
+
# @param text [#to_s] text to colorize
|
|
42
|
+
# @return [String] ANSI-wrapped text
|
|
23
43
|
def red(text) = wrap(text, "31")
|
|
44
|
+
|
|
45
|
+
# Apply blue foreground color (ANSI 34).
|
|
46
|
+
# @param text [#to_s] text to colorize
|
|
47
|
+
# @return [String] ANSI-wrapped text
|
|
24
48
|
def blue(text) = wrap(text, "34")
|
|
49
|
+
|
|
50
|
+
# Apply magenta foreground color (ANSI 35).
|
|
51
|
+
# @param text [#to_s] text to colorize
|
|
52
|
+
# @return [String] ANSI-wrapped text
|
|
25
53
|
def magenta(text) = wrap(text, "35")
|
|
54
|
+
|
|
55
|
+
# Apply white foreground color (ANSI 37).
|
|
56
|
+
# @param text [#to_s] text to colorize
|
|
57
|
+
# @return [String] ANSI-wrapped text
|
|
26
58
|
def white(text) = wrap(text, "37")
|
|
27
59
|
|
|
28
|
-
#
|
|
60
|
+
# @!endgroup
|
|
61
|
+
|
|
62
|
+
# @!group Text Styles
|
|
63
|
+
|
|
64
|
+
# Apply dim/faint style (ANSI 2).
|
|
65
|
+
# @param text [#to_s] text to style
|
|
66
|
+
# @return [String] ANSI-wrapped text
|
|
29
67
|
def dim(text) = wrap(text, "2")
|
|
68
|
+
|
|
69
|
+
# Apply bold style (ANSI 1).
|
|
70
|
+
# @param text [#to_s] text to style
|
|
71
|
+
# @return [String] ANSI-wrapped text
|
|
30
72
|
def bold(text) = wrap(text, "1")
|
|
73
|
+
|
|
74
|
+
# Apply italic style (ANSI 3).
|
|
75
|
+
# @param text [#to_s] text to style
|
|
76
|
+
# @return [String] ANSI-wrapped text
|
|
31
77
|
def italic(text) = wrap(text, "3")
|
|
78
|
+
|
|
79
|
+
# Apply underline style (ANSI 4).
|
|
80
|
+
# @param text [#to_s] text to style
|
|
81
|
+
# @return [String] ANSI-wrapped text
|
|
32
82
|
def underline(text) = wrap(text, "4")
|
|
83
|
+
|
|
84
|
+
# Apply inverse/reverse video style (ANSI 7).
|
|
85
|
+
# @param text [#to_s] text to style
|
|
86
|
+
# @return [String] ANSI-wrapped text
|
|
33
87
|
def inverse(text) = wrap(text, "7")
|
|
88
|
+
|
|
89
|
+
# Apply strikethrough style (ANSI 9).
|
|
90
|
+
# @param text [#to_s] text to style
|
|
91
|
+
# @return [String] ANSI-wrapped text
|
|
34
92
|
def strikethrough(text) = wrap(text, "9")
|
|
93
|
+
|
|
94
|
+
# Apply hidden/invisible style (ANSI 8).
|
|
95
|
+
# @param text [#to_s] text to style
|
|
96
|
+
# @return [String] ANSI-wrapped text
|
|
35
97
|
def hidden(text) = wrap(text, "8")
|
|
36
98
|
|
|
37
|
-
#
|
|
99
|
+
# @!endgroup
|
|
100
|
+
|
|
101
|
+
# @!group Bright Foreground Colors (high contrast)
|
|
102
|
+
|
|
103
|
+
# Apply bright cyan foreground color (ANSI 96).
|
|
104
|
+
# @param text [#to_s] text to colorize
|
|
105
|
+
# @return [String] ANSI-wrapped text
|
|
38
106
|
def bright_cyan(text) = wrap(text, "96")
|
|
107
|
+
|
|
108
|
+
# Apply bright green foreground color (ANSI 92).
|
|
109
|
+
# @param text [#to_s] text to colorize
|
|
110
|
+
# @return [String] ANSI-wrapped text
|
|
39
111
|
def bright_green(text) = wrap(text, "92")
|
|
112
|
+
|
|
113
|
+
# Apply bright yellow foreground color (ANSI 93).
|
|
114
|
+
# @param text [#to_s] text to colorize
|
|
115
|
+
# @return [String] ANSI-wrapped text
|
|
40
116
|
def bright_yellow(text) = wrap(text, "93")
|
|
117
|
+
|
|
118
|
+
# Apply bright red foreground color (ANSI 91).
|
|
119
|
+
# @param text [#to_s] text to colorize
|
|
120
|
+
# @return [String] ANSI-wrapped text
|
|
41
121
|
def bright_red(text) = wrap(text, "91")
|
|
122
|
+
|
|
123
|
+
# Apply bright blue foreground color (ANSI 94).
|
|
124
|
+
# @param text [#to_s] text to colorize
|
|
125
|
+
# @return [String] ANSI-wrapped text
|
|
42
126
|
def bright_blue(text) = wrap(text, "94")
|
|
127
|
+
|
|
128
|
+
# Apply bright magenta foreground color (ANSI 95).
|
|
129
|
+
# @param text [#to_s] text to colorize
|
|
130
|
+
# @return [String] ANSI-wrapped text
|
|
43
131
|
def bright_magenta(text) = wrap(text, "95")
|
|
132
|
+
|
|
133
|
+
# Apply bright white foreground color (ANSI 97).
|
|
134
|
+
# @param text [#to_s] text to colorize
|
|
135
|
+
# @return [String] ANSI-wrapped text
|
|
44
136
|
def bright_white(text) = wrap(text, "97")
|
|
45
137
|
|
|
138
|
+
# @!endgroup
|
|
139
|
+
|
|
46
140
|
private
|
|
47
141
|
|
|
48
142
|
def wrap(text, code)
|
data/lib/clack/core/cursor.rb
CHANGED
|
@@ -16,6 +16,11 @@ module Clack
|
|
|
16
16
|
SEQUENCE_TIMEOUT = 0.01
|
|
17
17
|
|
|
18
18
|
class << self
|
|
19
|
+
# Read a single keystroke from the terminal in raw mode.
|
|
20
|
+
# Handles multi-byte escape sequences (arrow keys, etc.).
|
|
21
|
+
#
|
|
22
|
+
# @return [String, nil] the key code, or nil on EOF
|
|
23
|
+
# @raise [IOError] if no console is available
|
|
19
24
|
def read
|
|
20
25
|
console = IO.console
|
|
21
26
|
raise IOError, "No console available (not a TTY?)" unless console
|
data/lib/clack/core/prompt.rb
CHANGED
|
@@ -41,6 +41,7 @@ module Clack
|
|
|
41
41
|
@active_prompts << prompt
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
# Unregister a prompt instance from resize notifications.
|
|
44
45
|
def unregister(prompt)
|
|
45
46
|
@active_prompts.delete(prompt)
|
|
46
47
|
end
|
|
@@ -109,7 +110,7 @@ module Clack
|
|
|
109
110
|
|
|
110
111
|
loop do
|
|
111
112
|
key = KeyReader.read
|
|
112
|
-
|
|
113
|
+
dispatch_key(key)
|
|
113
114
|
render
|
|
114
115
|
|
|
115
116
|
break if terminal_state?
|
|
@@ -124,17 +125,26 @@ module Clack
|
|
|
124
125
|
|
|
125
126
|
protected
|
|
126
127
|
|
|
127
|
-
#
|
|
128
|
-
#
|
|
128
|
+
# Dispatch a keypress, handling warning state before delegating to {#handle_key}.
|
|
129
|
+
#
|
|
130
|
+
# This method ensures ALL prompts participate in the warning validation flow,
|
|
131
|
+
# even those that override {#handle_key}. The run loop calls this instead of
|
|
132
|
+
# handle_key directly.
|
|
133
|
+
#
|
|
134
|
+
# Warning state behavior:
|
|
135
|
+
# - Enter confirms the warning and re-submits
|
|
136
|
+
# - Cancel (Escape/Ctrl+C) aborts from warning state
|
|
137
|
+
# - Any other input clears the warning and delegates to handle_key
|
|
138
|
+
#
|
|
139
|
+
# Also clears error state on any keypress, so subclasses don't need to
|
|
140
|
+
# manage error-to-active transitions themselves.
|
|
129
141
|
#
|
|
130
142
|
# @param key [String] the key code from {KeyReader}
|
|
131
|
-
def
|
|
143
|
+
def dispatch_key(key)
|
|
132
144
|
return if terminal_state?
|
|
133
145
|
|
|
134
|
-
action = Settings.action?(key)
|
|
135
|
-
|
|
136
|
-
# Handle warning state: Enter confirms, Cancel aborts, other input clears warning
|
|
137
146
|
if @state == :warning
|
|
147
|
+
action = Settings.action?(key)
|
|
138
148
|
case action
|
|
139
149
|
when :enter
|
|
140
150
|
confirm_warning
|
|
@@ -143,13 +153,34 @@ module Clack
|
|
|
143
153
|
@state = :cancel
|
|
144
154
|
else
|
|
145
155
|
clear_warning
|
|
146
|
-
|
|
156
|
+
handle_key(key)
|
|
147
157
|
end
|
|
148
158
|
return
|
|
149
159
|
end
|
|
150
160
|
|
|
151
161
|
@state = :active if @state == :error
|
|
152
162
|
|
|
163
|
+
handle_key(key)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Process a keypress and update state accordingly.
|
|
167
|
+
# Delegates to {#handle_input} for prompt-specific behavior.
|
|
168
|
+
#
|
|
169
|
+
# Override this in subclasses that need custom key dispatch (e.g., Select,
|
|
170
|
+
# Confirm, Autocomplete). The warning state and error-clearing are handled
|
|
171
|
+
# by {#dispatch_key} before this method is called, so overrides do not need
|
|
172
|
+
# to manage those transitions.
|
|
173
|
+
#
|
|
174
|
+
# For prompts that only need custom handling of printable/navigation input
|
|
175
|
+
# (not cancel/enter), override {#handle_input} instead. That is simpler and
|
|
176
|
+
# preserves the default cancel/enter behavior from this method.
|
|
177
|
+
#
|
|
178
|
+
# @param key [String] the key code from {KeyReader}
|
|
179
|
+
def handle_key(key)
|
|
180
|
+
return if terminal_state?
|
|
181
|
+
|
|
182
|
+
action = Settings.action?(key)
|
|
183
|
+
|
|
153
184
|
case action
|
|
154
185
|
when :cancel
|
|
155
186
|
@state = :cancel
|
|
@@ -162,6 +193,11 @@ module Clack
|
|
|
162
193
|
|
|
163
194
|
# Handle prompt-specific input. Override in subclasses.
|
|
164
195
|
#
|
|
196
|
+
# This is the simplest extension point for prompts that only need to handle
|
|
197
|
+
# navigation keys and printable input. Cancel and Enter are handled by
|
|
198
|
+
# {#handle_key}, and warning/error state transitions are handled by
|
|
199
|
+
# {#dispatch_key}.
|
|
200
|
+
#
|
|
165
201
|
# @param key [String] the raw key code
|
|
166
202
|
# @param action [Symbol, nil] the mapped action (:up, :down, etc.) or nil
|
|
167
203
|
def handle_input(key, action)
|
|
@@ -188,6 +224,14 @@ module Clack
|
|
|
188
224
|
# Validate a value and return the resulting state.
|
|
189
225
|
# Handles errors, warnings, and the warning confirmation flow.
|
|
190
226
|
#
|
|
227
|
+
# Validation contract: the validate proc receives the current value and returns:
|
|
228
|
+
# - nil or false: validation passes, prompt submits
|
|
229
|
+
# - String: hard error, prompt enters :error state and displays the message
|
|
230
|
+
# - Exception: hard error, the exception's message is displayed
|
|
231
|
+
# - Clack::Warning: soft warning, prompt enters :warning state. The user can
|
|
232
|
+
# press Enter to confirm and submit, or edit their input to clear the warning.
|
|
233
|
+
# - Any other truthy value: treated as an error (to_s is called for the message)
|
|
234
|
+
#
|
|
191
235
|
# @param value [Object] the value to validate
|
|
192
236
|
# @return [Symbol] :submit, :warning, or :error
|
|
193
237
|
def validate_value(value)
|
data/lib/clack/core/settings.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Clack
|
|
4
4
|
module Core
|
|
5
|
+
# Global configuration for key bindings, guide bar display, and input classification.
|
|
5
6
|
module Settings
|
|
6
7
|
# Navigation and control actions
|
|
7
8
|
ACTIONS = %i[up down left right space enter cancel].freeze
|
|
@@ -78,6 +79,9 @@ module Clack
|
|
|
78
79
|
@config_mutex.synchronize { @config[:with_guide] }
|
|
79
80
|
end
|
|
80
81
|
|
|
82
|
+
# Look up the action mapped to a key code.
|
|
83
|
+
# @param key [String] key code from {KeyReader}
|
|
84
|
+
# @return [Symbol, nil] the action (:up, :down, :enter, etc.) or nil
|
|
81
85
|
def action?(key)
|
|
82
86
|
aliases = @config_mutex.synchronize { @config[:aliases] }
|
|
83
87
|
aliases[key] if ACTIONS.include?(aliases[key])
|
|
@@ -25,10 +25,14 @@ module Clack
|
|
|
25
25
|
format_placeholder_with_cursor(text)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
# @return [String, nil] the placeholder text, or nil if none set
|
|
28
29
|
def current_placeholder
|
|
29
30
|
defined?(@placeholder) ? @placeholder : nil
|
|
30
31
|
end
|
|
31
32
|
|
|
33
|
+
# Render placeholder text with an inverse cursor on the first character.
|
|
34
|
+
# @param text [String] placeholder text to format
|
|
35
|
+
# @return [String] formatted placeholder with cursor highlight
|
|
32
36
|
def format_placeholder_with_cursor(text)
|
|
33
37
|
chars = text.grapheme_clusters
|
|
34
38
|
first = chars.first || ""
|