Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .reek.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,4 @@ detectors:

exclude_paths:
- spec
- examples
2 changes: 1 addition & 1 deletion clack.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Gem::Specification.new do |spec|
"rubygems_mfa_required" => "true"
}

spec.files = Dir["lib/**/*", "LICENSE", "README.md", "CHANGELOG.md"]
spec.files = Dir["lib/**/*", "examples/**/*", "LICENSE", "README.md", "CHANGELOG.md"]
spec.bindir = "exe"
spec.executables = ["clack-demo"]
spec.require_paths = ["lib"]
Expand Down
172 changes: 171 additions & 1 deletion examples/demo.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,174 @@

require_relative "../lib/clack"

Clack.demo
def run_demo
Clack.intro "clack-demo"

result = Clack.group(on_cancel: ->(_) { Clack.cancel("Operation cancelled.") }) do |g|
g.prompt(:name) do
Clack.text(
message: "What is your project named?",
placeholder: "my-app",
validate: ->(v) { "Project name is required" if v.to_s.strip.empty? }
)
end

g.prompt(:directory) do |r|
Clack.text(
message: "Where should we create your project?",
initial_value: "./#{r[:name]}"
)
end

g.prompt(:template) do
Clack.select(
message: "Which template would you like to use?",
options: [
{value: "default", label: "Default", hint: "recommended"},
{value: "minimal", label: "Minimal", hint: "bare bones"},
{value: "api", label: "API Only", hint: "no frontend"},
{value: "full", label: "Full Stack", hint: "everything included"}
]
)
end

g.prompt(:typescript) do
Clack.confirm(
message: "Would you like to use TypeScript?",
initial_value: true
)
end

g.prompt(:features) do
Clack.multiselect(
message: "Which features would you like to include?",
options: [
{value: "eslint", label: "ESLint", hint: "code linting"},
{value: "prettier", label: "Prettier", hint: "code formatting"},
{value: "tailwind", label: "Tailwind CSS", hint: "utility-first CSS"},
{value: "docker", label: "Docker", hint: "containerization"},
{value: "ci", label: "GitHub Actions", hint: "CI/CD pipeline"}
],
initial_values: %w[eslint prettier],
required: false
)
end

g.prompt(:package_manager) do
Clack.select(
message: "Which package manager do you prefer?",
options: [
{value: "npm", label: "npm"},
{value: "yarn", label: "yarn"},
{value: "pnpm", label: "pnpm", hint: "recommended"},
{value: "bun", label: "bun", hint: "fast"}
],
initial_value: "pnpm"
)
end
end

return if Clack.handle_cancel(result)

# Autocomplete prompt
color = Clack.autocomplete(
message: "Pick a theme color:",
options: %w[red orange yellow green blue indigo violet pink cyan magenta]
)
return if Clack.handle_cancel(color)

# Select key prompt (quick keyboard shortcuts)
action = Clack.select_key(
message: "What would you like to do first?",
options: [
{value: "dev", label: "Start dev server", key: "d"},
{value: "build", label: "Build for production", key: "b"},
{value: "test", label: "Run tests", key: "t"}
]
)
return if Clack.handle_cancel(action)

# Path prompt
config_path = Clack.path(
message: "Select config directory:",
only_directories: true
)
return if Clack.handle_cancel(config_path)

# Group multiselect
stack = Clack.group_multiselect(
message: "Select additional integrations:",
options: [
{
label: "Frontend",
options: [
{value: "react", label: "React"},
{value: "vue", label: "Vue"},
{value: "svelte", label: "Svelte"}
]
},
{
label: "Backend",
options: [
{value: "express", label: "Express"},
{value: "fastify", label: "Fastify"},
{value: "hono", label: "Hono"}
]
},
{
label: "Database",
options: [
{value: "postgres", label: "PostgreSQL"},
{value: "mysql", label: "MySQL"},
{value: "sqlite", label: "SQLite"}
]
}
],
required: false
)
return if Clack.handle_cancel(stack)

# Progress bar
prog = Clack.progress(total: 100, message: "Downloading assets...")
prog.start
20.times do
sleep 0.03
prog.advance(5)
end
prog.stop("Assets downloaded!")

# Tasks
Clack.tasks(tasks: [
{title: "Validating configuration", task: -> { sleep 0.3 }},
{title: "Generating types", task: -> { sleep 0.4 }},
{title: "Compiling assets", task: -> { sleep 0.3 }}
])

# Spinner
s = Clack.spinner
s.start "Installing dependencies via #{result[:package_manager]}..."
sleep 1.0
s.message "Configuring #{result[:template]} template..."
sleep 0.6
s.stop "Project created successfully!"

# Summary
Clack.log.step "Project: #{result[:name]}"
Clack.log.step "Directory: #{result[:directory]}"
Clack.log.step "Template: #{result[:template]}"
Clack.log.step "TypeScript: #{result[:typescript] ? "Yes" : "No"}"
Clack.log.step "Features: #{result[:features].join(", ")}" unless result[:features].empty?
Clack.log.step "Color: #{color}"
Clack.log.step "Action: #{action}"
Clack.log.step "Config: #{config_path}"
Clack.log.step "Stack: #{stack.join(", ")}" unless stack.empty?

Clack.note <<~MSG, title: "Next steps"
cd #{result[:directory]}
#{result[:package_manager]} run dev
MSG

Clack.outro "Happy coding!"
end

run_demo if __FILE__ == $PROGRAM_NAME
1 change: 1 addition & 0 deletions lefthook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ pre-commit:
stage_fixed: true
reek:
glob: "*.rb"
exclude: "examples/"
run: bundle exec reek {staged_files}
180 changes: 8 additions & 172 deletions lib/clack.rb
Original file line number Diff line number Diff line change
Expand Up @@ -424,179 +424,15 @@ def rows(output = $stdout, default: 24)
Environment.rows(output, default: default)
end

# :nocov:
# :reek:TooManyStatements :reek:NestedIterators :reek:UncommunicativeVariableName
# Demo - showcases all Clack features (interactive, tested manually)
# Run the interactive demo showcasing all Clack features.
# The demo implementation is in examples/demo.rb.
#
# @return [void]
def demo
intro "clack-demo"

result = group(on_cancel: ->(_) { cancel("Operation cancelled.") }) do |g|
g.prompt(:name) do
text(
message: "What is your project named?",
placeholder: "my-app",
validate: ->(v) { "Project name is required" if v.to_s.strip.empty? }
)
end

g.prompt(:directory) do |r|
text(
message: "Where should we create your project?",
initial_value: "./#{r[:name]}"
)
end

g.prompt(:template) do
select(
message: "Which template would you like to use?",
options: [
{value: "default", label: "Default", hint: "recommended"},
{value: "minimal", label: "Minimal", hint: "bare bones"},
{value: "api", label: "API Only", hint: "no frontend"},
{value: "full", label: "Full Stack", hint: "everything included"}
]
)
end

g.prompt(:typescript) do
confirm(
message: "Would you like to use TypeScript?",
initial_value: true
)
end

g.prompt(:features) do
multiselect(
message: "Which features would you like to include?",
options: [
{value: "eslint", label: "ESLint", hint: "code linting"},
{value: "prettier", label: "Prettier", hint: "code formatting"},
{value: "tailwind", label: "Tailwind CSS", hint: "utility-first CSS"},
{value: "docker", label: "Docker", hint: "containerization"},
{value: "ci", label: "GitHub Actions", hint: "CI/CD pipeline"}
],
initial_values: %w[eslint prettier],
required: false
)
end

g.prompt(:package_manager) do
select(
message: "Which package manager do you prefer?",
options: [
{value: "npm", label: "npm"},
{value: "yarn", label: "yarn"},
{value: "pnpm", label: "pnpm", hint: "recommended"},
{value: "bun", label: "bun", hint: "fast"}
],
initial_value: "pnpm"
)
end
end

return if cancel?(result)

# Autocomplete prompt
color = autocomplete(
message: "Pick a theme color:",
options: %w[red orange yellow green blue indigo violet pink cyan magenta]
)
return if handle_cancel(color)

# Select key prompt (quick keyboard shortcuts)
action = select_key(
message: "What would you like to do first?",
options: [
{value: "dev", label: "Start dev server", key: "d"},
{value: "build", label: "Build for production", key: "b"},
{value: "test", label: "Run tests", key: "t"}
]
)
return if handle_cancel(action)

# Path prompt
config_path = path(
message: "Select config directory:",
only_directories: true
)
return if handle_cancel(config_path)

# Group multiselect
stack = group_multiselect(
message: "Select additional integrations:",
options: [
{
label: "Frontend",
options: [
{value: "react", label: "React"},
{value: "vue", label: "Vue"},
{value: "svelte", label: "Svelte"}
]
},
{
label: "Backend",
options: [
{value: "express", label: "Express"},
{value: "fastify", label: "Fastify"},
{value: "hono", label: "Hono"}
]
},
{
label: "Database",
options: [
{value: "postgres", label: "PostgreSQL"},
{value: "mysql", label: "MySQL"},
{value: "sqlite", label: "SQLite"}
]
}
],
required: false
)
return if handle_cancel(stack)

# Progress bar
prog = progress(total: 100, message: "Downloading assets...")
prog.start
20.times do
sleep 0.03
prog.advance(5)
end
prog.stop("Assets downloaded!")

# Tasks
tasks(tasks: [
{title: "Validating configuration", task: -> { sleep 0.3 }},
{title: "Generating types", task: -> { sleep 0.4 }},
{title: "Compiling assets", task: -> { sleep 0.3 }}
])

# Spinner
s = spinner
s.start "Installing dependencies via #{result[:package_manager]}..."
sleep 1.0
s.message "Configuring #{result[:template]} template..."
sleep 0.6
s.stop "Project created successfully!"

# Summary
log.step "Project: #{result[:name]}"
log.step "Directory: #{result[:directory]}"
log.step "Template: #{result[:template]}"
log.step "TypeScript: #{result[:typescript] ? "Yes" : "No"}"
log.step "Features: #{result[:features].join(", ")}" unless result[:features].empty?
log.step "Color: #{color}"
log.step "Action: #{action}"
log.step "Config: #{config_path}"
log.step "Stack: #{stack.join(", ")}" unless stack.empty?

note <<~MSG, title: "Next steps"
cd #{result[:directory]}
#{result[:package_manager]} run dev
MSG

outro "Happy coding!"
end
# :nocov:
demo_path = File.expand_path("../examples/demo.rb", __dir__)
load demo_path
run_demo
end
end
end

Expand Down