diff --git a/.reek.yml b/.reek.yml index bba7115..5b307b3 100644 --- a/.reek.yml +++ b/.reek.yml @@ -72,3 +72,4 @@ detectors: exclude_paths: - spec + - examples diff --git a/clack.gemspec b/clack.gemspec index cf8433d..2956022 100644 --- a/clack.gemspec +++ b/clack.gemspec @@ -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"] diff --git a/examples/demo.rb b/examples/demo.rb index b938e40..531d152 100755 --- a/examples/demo.rb +++ b/examples/demo.rb @@ -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 diff --git a/lefthook.yml b/lefthook.yml index 51b2e0e..52d63d7 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -7,4 +7,5 @@ pre-commit: stage_fixed: true reek: glob: "*.rb" + exclude: "examples/" run: bundle exec reek {staged_files} diff --git a/lib/clack.rb b/lib/clack.rb index e00b6b3..3d669b2 100644 --- a/lib/clack.rb +++ b/lib/clack.rb @@ -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