shopify-cli 0.9.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/.github/CODEOWNERS +1 -0
- data/.github/CODE_OF_CONDUCT.md +73 -0
- data/.github/CONTRIBUTING.md +51 -0
- data/.github/DESIGN.md +153 -0
- data/.github/ISSUE_TEMPLATE.md +38 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +22 -0
- data/.github/probots.yml +3 -0
- data/.gitignore +19 -0
- data/.rubocop.yml +47 -0
- data/.ruby-version +1 -0
- data/.travis.yml +12 -0
- data/Gemfile +22 -0
- data/Gemfile.lock +77 -0
- data/LICENSE.md +7 -0
- data/README.md +13 -0
- data/Rakefile +101 -0
- data/SECURITY.md +59 -0
- data/Vagrantfile +17 -0
- data/bin/load_shopify.rb +20 -0
- data/bin/shopify +32 -0
- data/dev.yml +17 -0
- data/docs/Gemfile +5 -0
- data/docs/Gemfile.lock +248 -0
- data/docs/_config.yml +16 -0
- data/docs/_data/nav.yml +26 -0
- data/docs/_includes/footer.html +15 -0
- data/docs/_includes/head.html +19 -0
- data/docs/_includes/sidebar_nav.html +22 -0
- data/docs/_includes/toc.html +112 -0
- data/docs/_layouts/default.html +79 -0
- data/docs/app/node/commands/index.md +82 -0
- data/docs/app/node/index.md +35 -0
- data/docs/app/rails/commands/index.md +80 -0
- data/docs/app/rails/index.md +36 -0
- data/docs/core/index.md +70 -0
- data/docs/css/docs.css +157 -0
- data/docs/getting-started/index.md +61 -0
- data/docs/help/start-app/index.md +6 -0
- data/docs/images/header.png +0 -0
- data/docs/index.md +27 -0
- data/docs/installing-ruby.md +28 -0
- data/ext/shopify-cli/extconf.rb +27 -0
- data/install.sh +7 -0
- data/lib/docgen/class_template.md.erb +81 -0
- data/lib/docgen/index_template.md.erb +5 -0
- data/lib/docgen/markdown.rb +101 -0
- data/lib/graphql/admin_introspection.graphql +87 -0
- data/lib/graphql/all_organizations.graphql +19 -0
- data/lib/graphql/all_orgs_with_apps.graphql +30 -0
- data/lib/graphql/api_versions.graphql +6 -0
- data/lib/graphql/convert_dev_to_test_store.graphql +10 -0
- data/lib/graphql/create_app.graphql +20 -0
- data/lib/graphql/create_customer.graphql +9 -0
- data/lib/graphql/create_draft_order.graphql +8 -0
- data/lib/graphql/create_product.graphql +9 -0
- data/lib/graphql/extension_create.graphql +21 -0
- data/lib/graphql/extension_update_draft.graphql +18 -0
- data/lib/graphql/find_organization.graphql +17 -0
- data/lib/graphql/get_app_urls.graphql +6 -0
- data/lib/graphql/update_dashboard_urls.graphql +8 -0
- data/lib/project_types/extension/cli.rb +71 -0
- data/lib/project_types/extension/commands/build.rb +29 -0
- data/lib/project_types/extension/commands/create.rb +49 -0
- data/lib/project_types/extension/commands/extension_command.rb +22 -0
- data/lib/project_types/extension/commands/push.rb +69 -0
- data/lib/project_types/extension/commands/register.rb +78 -0
- data/lib/project_types/extension/commands/serve.rb +24 -0
- data/lib/project_types/extension/commands/tunnel.rb +69 -0
- data/lib/project_types/extension/extension_project.rb +85 -0
- data/lib/project_types/extension/extension_project_keys.rb +10 -0
- data/lib/project_types/extension/features/argo.rb +48 -0
- data/lib/project_types/extension/features/argo_dependencies.rb +28 -0
- data/lib/project_types/extension/features/argo_setup.rb +54 -0
- data/lib/project_types/extension/features/argo_setup_step.rb +31 -0
- data/lib/project_types/extension/features/argo_setup_steps.rb +53 -0
- data/lib/project_types/extension/features/tunnel_url.rb +20 -0
- data/lib/project_types/extension/forms/create.rb +52 -0
- data/lib/project_types/extension/forms/register.rb +48 -0
- data/lib/project_types/extension/messages/message_loading.rb +37 -0
- data/lib/project_types/extension/messages/messages.rb +126 -0
- data/lib/project_types/extension/models/app.rb +14 -0
- data/lib/project_types/extension/models/registration.rb +19 -0
- data/lib/project_types/extension/models/type.rb +76 -0
- data/lib/project_types/extension/models/types/checkout_post_purchase.rb +20 -0
- data/lib/project_types/extension/models/types/subscription_management.rb +20 -0
- data/lib/project_types/extension/models/validation_error.rb +17 -0
- data/lib/project_types/extension/models/version.rb +15 -0
- data/lib/project_types/extension/tasks/converters/registration_converter.rb +26 -0
- data/lib/project_types/extension/tasks/converters/validation_error_converter.rb +25 -0
- data/lib/project_types/extension/tasks/converters/version_converter.rb +28 -0
- data/lib/project_types/extension/tasks/create_extension.rb +31 -0
- data/lib/project_types/extension/tasks/get_apps.rb +34 -0
- data/lib/project_types/extension/tasks/update_draft.rb +29 -0
- data/lib/project_types/extension/tasks/user_errors.rb +45 -0
- data/lib/project_types/node/cli.rb +37 -0
- data/lib/project_types/node/commands/create.rb +117 -0
- data/lib/project_types/node/commands/deploy.rb +22 -0
- data/lib/project_types/node/commands/deploy/heroku.rb +91 -0
- data/lib/project_types/node/commands/generate.rb +51 -0
- data/lib/project_types/node/commands/generate/billing.rb +37 -0
- data/lib/project_types/node/commands/generate/page.rb +55 -0
- data/lib/project_types/node/commands/generate/webhook.rb +33 -0
- data/lib/project_types/node/commands/open.rb +16 -0
- data/lib/project_types/node/commands/populate.rb +23 -0
- data/lib/project_types/node/commands/populate/customer.rb +31 -0
- data/lib/project_types/node/commands/populate/draft_order.rb +28 -0
- data/lib/project_types/node/commands/populate/product.rb +30 -0
- data/lib/project_types/node/commands/serve.rb +45 -0
- data/lib/project_types/node/commands/tunnel.rb +39 -0
- data/lib/project_types/node/forms/create.rb +87 -0
- data/lib/project_types/node/messages/messages.rb +260 -0
- data/lib/project_types/rails/cli.rb +41 -0
- data/lib/project_types/rails/commands/create.rb +126 -0
- data/lib/project_types/rails/commands/deploy.rb +22 -0
- data/lib/project_types/rails/commands/deploy/heroku.rb +113 -0
- data/lib/project_types/rails/commands/generate.rb +49 -0
- data/lib/project_types/rails/commands/generate/webhook.rb +39 -0
- data/lib/project_types/rails/commands/open.rb +16 -0
- data/lib/project_types/rails/commands/populate.rb +23 -0
- data/lib/project_types/rails/commands/populate/customer.rb +31 -0
- data/lib/project_types/rails/commands/populate/draft_order.rb +28 -0
- data/lib/project_types/rails/commands/populate/product.rb +30 -0
- data/lib/project_types/rails/commands/serve.rb +47 -0
- data/lib/project_types/rails/commands/tunnel.rb +39 -0
- data/lib/project_types/rails/forms/create.rb +116 -0
- data/lib/project_types/rails/gem.rb +56 -0
- data/lib/project_types/rails/messages/messages.rb +283 -0
- data/lib/project_types/rails/ruby.rb +17 -0
- data/lib/project_types/script/cli.rb +76 -0
- data/lib/project_types/script/commands/create.rb +45 -0
- data/lib/project_types/script/commands/disable.rb +36 -0
- data/lib/project_types/script/commands/enable.rb +46 -0
- data/lib/project_types/script/commands/push.rb +39 -0
- data/lib/project_types/script/config/extension_points.yml +18 -0
- data/lib/project_types/script/errors.rb +16 -0
- data/lib/project_types/script/forms/create.rb +29 -0
- data/lib/project_types/script/forms/enable.rb +24 -0
- data/lib/project_types/script/forms/push.rb +19 -0
- data/lib/project_types/script/forms/script_form.rb +66 -0
- data/lib/project_types/script/graphql/app_script_update_or_create.graphql +27 -0
- data/lib/project_types/script/graphql/script_service_proxy.graphql +8 -0
- data/lib/project_types/script/graphql/shop_script_delete.graphql +14 -0
- data/lib/project_types/script/graphql/shop_script_update_or_create.graphql +28 -0
- data/lib/project_types/script/layers/application/build_script.rb +43 -0
- data/lib/project_types/script/layers/application/create_script.rb +47 -0
- data/lib/project_types/script/layers/application/disable_script.rb +19 -0
- data/lib/project_types/script/layers/application/enable_script.rb +21 -0
- data/lib/project_types/script/layers/application/extension_points.rb +17 -0
- data/lib/project_types/script/layers/application/project_dependencies.rb +34 -0
- data/lib/project_types/script/layers/application/push_script.rb +30 -0
- data/lib/project_types/script/layers/domain/errors.rb +25 -0
- data/lib/project_types/script/layers/domain/extension_point.rb +29 -0
- data/lib/project_types/script/layers/domain/push_package.rb +29 -0
- data/lib/project_types/script/layers/domain/script.rb +18 -0
- data/lib/project_types/script/layers/infrastructure/assemblyscript_dependency_manager.rb +73 -0
- data/lib/project_types/script/layers/infrastructure/assemblyscript_tsconfig.rb +38 -0
- data/lib/project_types/script/layers/infrastructure/assemblyscript_wasm_builder.rb +39 -0
- data/lib/project_types/script/layers/infrastructure/dependency_manager.rb +36 -0
- data/lib/project_types/script/layers/infrastructure/errors.rb +38 -0
- data/lib/project_types/script/layers/infrastructure/extension_point_repository.rb +31 -0
- data/lib/project_types/script/layers/infrastructure/push_package_repository.rb +47 -0
- data/lib/project_types/script/layers/infrastructure/script_builder.rb +34 -0
- data/lib/project_types/script/layers/infrastructure/script_repository.rb +89 -0
- data/lib/project_types/script/layers/infrastructure/script_service.rb +165 -0
- data/lib/project_types/script/layers/infrastructure/test_suite_repository.rb +59 -0
- data/lib/project_types/script/messages/messages.rb +204 -0
- data/lib/project_types/script/script_project.rb +37 -0
- data/lib/project_types/script/templates/ts/as-pect.config.js +21 -0
- data/lib/project_types/script/ui/error_handler.rb +136 -0
- data/lib/project_types/script/ui/strict_spinner.rb +22 -0
- data/lib/rubygems_plugin.rb +18 -0
- data/lib/shopify-cli/admin_api.rb +99 -0
- data/lib/shopify-cli/admin_api/populate_resource_command.rb +165 -0
- data/lib/shopify-cli/admin_api/schema.rb +32 -0
- data/lib/shopify-cli/api.rb +104 -0
- data/lib/shopify-cli/command.rb +67 -0
- data/lib/shopify-cli/commands.rb +28 -0
- data/lib/shopify-cli/commands/connect.rb +108 -0
- data/lib/shopify-cli/commands/create.rb +50 -0
- data/lib/shopify-cli/commands/help.rb +79 -0
- data/lib/shopify-cli/commands/logout.rb +23 -0
- data/lib/shopify-cli/commands/system.rb +135 -0
- data/lib/shopify-cli/commands/version.rb +15 -0
- data/lib/shopify-cli/context.rb +372 -0
- data/lib/shopify-cli/core.rb +9 -0
- data/lib/shopify-cli/core/entry_point.rb +40 -0
- data/lib/shopify-cli/core/executor.rb +21 -0
- data/lib/shopify-cli/core/help_resolver.rb +20 -0
- data/lib/shopify-cli/core/monorail.rb +118 -0
- data/lib/shopify-cli/db.rb +114 -0
- data/lib/shopify-cli/form.rb +40 -0
- data/lib/shopify-cli/git.rb +141 -0
- data/lib/shopify-cli/helpers.rb +5 -0
- data/lib/shopify-cli/helpers/haikunator.rb +92 -0
- data/lib/shopify-cli/heroku.rb +97 -0
- data/lib/shopify-cli/js_deps.rb +110 -0
- data/lib/shopify-cli/js_system.rb +98 -0
- data/lib/shopify-cli/messages/messages.rb +287 -0
- data/lib/shopify-cli/oauth.rb +192 -0
- data/lib/shopify-cli/oauth/servlet.rb +61 -0
- data/lib/shopify-cli/options.rb +40 -0
- data/lib/shopify-cli/packager.rb +116 -0
- data/lib/shopify-cli/partners_api.rb +114 -0
- data/lib/shopify-cli/partners_api/organizations.rb +32 -0
- data/lib/shopify-cli/process_supervision.rb +187 -0
- data/lib/shopify-cli/project.rb +191 -0
- data/lib/shopify-cli/project_type.rb +83 -0
- data/lib/shopify-cli/resources.rb +5 -0
- data/lib/shopify-cli/resources/env_file.rb +96 -0
- data/lib/shopify-cli/sub_command.rb +15 -0
- data/lib/shopify-cli/task.rb +10 -0
- data/lib/shopify-cli/tasks.rb +32 -0
- data/lib/shopify-cli/tasks/create_api_client.rb +29 -0
- data/lib/shopify-cli/tasks/ensure_dev_store.rb +41 -0
- data/lib/shopify-cli/tasks/ensure_env.rb +31 -0
- data/lib/shopify-cli/tasks/ensure_loopback_url.rb +20 -0
- data/lib/shopify-cli/tasks/update_dashboard_urls.rb +44 -0
- data/lib/shopify-cli/tunnel.rb +154 -0
- data/lib/shopify-cli/version.rb +3 -0
- data/lib/shopify_cli.rb +132 -0
- data/shopify-cli.gemspec +40 -0
- data/shopify.fish +12 -0
- data/shopify.sh +11 -0
- data/vendor/deps/cli-kit/REVISION +1 -0
- data/vendor/deps/cli-kit/lib/cli/kit.rb +60 -0
- data/vendor/deps/cli-kit/lib/cli/kit/autocall.rb +21 -0
- data/vendor/deps/cli-kit/lib/cli/kit/base_command.rb +49 -0
- data/vendor/deps/cli-kit/lib/cli/kit/command_registry.rb +94 -0
- data/vendor/deps/cli-kit/lib/cli/kit/config.rb +133 -0
- data/vendor/deps/cli-kit/lib/cli/kit/error_handler.rb +115 -0
- data/vendor/deps/cli-kit/lib/cli/kit/executor.rb +81 -0
- data/vendor/deps/cli-kit/lib/cli/kit/ini.rb +102 -0
- data/vendor/deps/cli-kit/lib/cli/kit/levenshtein.rb +82 -0
- data/vendor/deps/cli-kit/lib/cli/kit/logger.rb +76 -0
- data/vendor/deps/cli-kit/lib/cli/kit/resolver.rb +60 -0
- data/vendor/deps/cli-kit/lib/cli/kit/ruby_backports/enumerable.rb +6 -0
- data/vendor/deps/cli-kit/lib/cli/kit/support.rb +9 -0
- data/vendor/deps/cli-kit/lib/cli/kit/support/test_helper.rb +244 -0
- data/vendor/deps/cli-kit/lib/cli/kit/system.rb +207 -0
- data/vendor/deps/cli-kit/lib/cli/kit/util.rb +189 -0
- data/vendor/deps/cli-kit/lib/cli/kit/version.rb +5 -0
- data/vendor/deps/cli-ui/REVISION +1 -0
- data/vendor/deps/cli-ui/lib/cli/ui.rb +187 -0
- data/vendor/deps/cli-ui/lib/cli/ui/ansi.rb +153 -0
- data/vendor/deps/cli-ui/lib/cli/ui/box.rb +15 -0
- data/vendor/deps/cli-ui/lib/cli/ui/color.rb +79 -0
- data/vendor/deps/cli-ui/lib/cli/ui/formatter.rb +179 -0
- data/vendor/deps/cli-ui/lib/cli/ui/frame.rb +310 -0
- data/vendor/deps/cli-ui/lib/cli/ui/glyph.rb +78 -0
- data/vendor/deps/cli-ui/lib/cli/ui/progress.rb +88 -0
- data/vendor/deps/cli-ui/lib/cli/ui/prompt.rb +248 -0
- data/vendor/deps/cli-ui/lib/cli/ui/prompt/interactive_options.rb +472 -0
- data/vendor/deps/cli-ui/lib/cli/ui/prompt/options_handler.rb +24 -0
- data/vendor/deps/cli-ui/lib/cli/ui/spinner.rb +48 -0
- data/vendor/deps/cli-ui/lib/cli/ui/spinner/async.rb +40 -0
- data/vendor/deps/cli-ui/lib/cli/ui/spinner/spin_group.rb +241 -0
- data/vendor/deps/cli-ui/lib/cli/ui/stdout_router.rb +227 -0
- data/vendor/deps/cli-ui/lib/cli/ui/terminal.rb +36 -0
- data/vendor/deps/cli-ui/lib/cli/ui/truncater.rb +102 -0
- data/vendor/deps/cli-ui/lib/cli/ui/version.rb +5 -0
- data/vendor/deps/smart_properties/REVISION +1 -0
- data/vendor/deps/smart_properties/lib/smart_properties.rb +174 -0
- data/vendor/deps/smart_properties/lib/smart_properties/errors.rb +114 -0
- data/vendor/deps/smart_properties/lib/smart_properties/property.rb +162 -0
- data/vendor/deps/smart_properties/lib/smart_properties/property_collection.rb +83 -0
- data/vendor/deps/smart_properties/lib/smart_properties/validations.rb +8 -0
- data/vendor/deps/smart_properties/lib/smart_properties/validations/ancestor.rb +27 -0
- data/vendor/deps/smart_properties/lib/smart_properties/version.rb +3 -0
- data/vendor/lib/semantic/LICENSE +20 -0
- data/vendor/lib/semantic/semantic.rb +4 -0
- data/vendor/lib/semantic/version.rb +180 -0
- metadata +374 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
require 'cli/ui'
|
|
2
|
+
|
|
3
|
+
module CLI
|
|
4
|
+
module UI
|
|
5
|
+
class Glyph
|
|
6
|
+
class InvalidGlyphHandle < ArgumentError
|
|
7
|
+
def initialize(handle)
|
|
8
|
+
@handle = handle
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def message
|
|
12
|
+
keys = Glyph.available.join(',')
|
|
13
|
+
"invalid glyph handle: #{@handle} " \
|
|
14
|
+
"-- must be one of CLI::UI::Glyph.available (#{keys})"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
attr_reader :handle, :codepoint, :color, :char, :to_s, :fmt
|
|
19
|
+
|
|
20
|
+
# Creates a new glyph
|
|
21
|
+
#
|
|
22
|
+
# ==== Attributes
|
|
23
|
+
#
|
|
24
|
+
# * +handle+ - The handle in the +MAP+ constant
|
|
25
|
+
# * +codepoint+ - The codepoint used to create the glyph (e.g. +0x2717+ for a ballot X)
|
|
26
|
+
# * +color+ - What color to output the glyph. Check +CLI::UI::Color+ for options.
|
|
27
|
+
#
|
|
28
|
+
def initialize(handle, codepoint, color)
|
|
29
|
+
@handle = handle
|
|
30
|
+
@codepoint = codepoint
|
|
31
|
+
@color = color
|
|
32
|
+
@char = [codepoint].pack('U')
|
|
33
|
+
@to_s = color.code + char + Color::RESET.code
|
|
34
|
+
@fmt = "{{#{color.name}:#{char}}}"
|
|
35
|
+
|
|
36
|
+
MAP[handle] = self
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Mapping of glyphs to terminal output
|
|
40
|
+
MAP = {}
|
|
41
|
+
# YELLOw SMALL STAR (⭑)
|
|
42
|
+
STAR = new('*', 0x2b51, Color::YELLOW)
|
|
43
|
+
# BLUE MATHEMATICAL SCRIPT SMALL i (𝒾)
|
|
44
|
+
INFO = new('i', 0x1d4be, Color::BLUE)
|
|
45
|
+
# BLUE QUESTION MARK (?)
|
|
46
|
+
QUESTION = new('?', 0x003f, Color::BLUE)
|
|
47
|
+
# GREEN CHECK MARK (✔︎)
|
|
48
|
+
CHECK = new('v', 0x2713, Color::GREEN)
|
|
49
|
+
# RED BALLOT X (✗)
|
|
50
|
+
X = new('x', 0x2717, Color::RED)
|
|
51
|
+
# Bug emoji (🐛)
|
|
52
|
+
BUG = new('b', 0x1f41b, Color::WHITE)
|
|
53
|
+
# RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK (»)
|
|
54
|
+
CHEVRON = new('>', 0xbb, Color::YELLOW)
|
|
55
|
+
|
|
56
|
+
# Looks up a glyph by name
|
|
57
|
+
#
|
|
58
|
+
# ==== Raises
|
|
59
|
+
# Raises a InvalidGlyphHandle if the glyph is not available
|
|
60
|
+
# You likely need to create it with +.new+ or you made a typo
|
|
61
|
+
#
|
|
62
|
+
# ==== Returns
|
|
63
|
+
# Returns a terminal output-capable string
|
|
64
|
+
#
|
|
65
|
+
def self.lookup(name)
|
|
66
|
+
MAP.fetch(name.to_s)
|
|
67
|
+
rescue KeyError
|
|
68
|
+
raise InvalidGlyphHandle, name
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# All available glyphs by name
|
|
72
|
+
#
|
|
73
|
+
def self.available
|
|
74
|
+
MAP.keys
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
require 'cli/ui'
|
|
2
|
+
|
|
3
|
+
module CLI
|
|
4
|
+
module UI
|
|
5
|
+
class Progress
|
|
6
|
+
# A Cyan filled block
|
|
7
|
+
FILLED_BAR = "\e[46m"
|
|
8
|
+
# A bright white block
|
|
9
|
+
UNFILLED_BAR = "\e[1;47m"
|
|
10
|
+
|
|
11
|
+
# Add a progress bar to the terminal output
|
|
12
|
+
#
|
|
13
|
+
# https://user-images.githubusercontent.com/3074765/33799794-cc4c940e-dd00-11e7-9bdc-90f77ec9167c.gif
|
|
14
|
+
#
|
|
15
|
+
# ==== Example Usage:
|
|
16
|
+
#
|
|
17
|
+
# Set the percent to X
|
|
18
|
+
# CLI::UI::Progress.progress do |bar|
|
|
19
|
+
# bar.tick(set_percent: percent)
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# Increase the percent by 1 percent
|
|
23
|
+
# CLI::UI::Progress.progress do |bar|
|
|
24
|
+
# bar.tick
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# Increase the percent by X
|
|
28
|
+
# CLI::UI::Progress.progress do |bar|
|
|
29
|
+
# bar.tick(percent: 5)
|
|
30
|
+
# end
|
|
31
|
+
def self.progress(width: Terminal.width)
|
|
32
|
+
bar = Progress.new(width: width)
|
|
33
|
+
print CLI::UI::ANSI.hide_cursor
|
|
34
|
+
yield(bar)
|
|
35
|
+
ensure
|
|
36
|
+
puts bar.to_s
|
|
37
|
+
CLI::UI.raw do
|
|
38
|
+
print(ANSI.show_cursor)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Initialize a progress bar. Typically used in a +Progress.progress+ block
|
|
43
|
+
#
|
|
44
|
+
# ==== Options
|
|
45
|
+
# One of the follow can be used, but not both together
|
|
46
|
+
#
|
|
47
|
+
# * +:width+ - The width of the terminal
|
|
48
|
+
#
|
|
49
|
+
def initialize(width: Terminal.width)
|
|
50
|
+
@percent_done = 0
|
|
51
|
+
@max_width = width
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Set the progress of the bar. Typically used in a +Progress.progress+ block
|
|
55
|
+
#
|
|
56
|
+
# ==== Options
|
|
57
|
+
# One of the follow can be used, but not both together
|
|
58
|
+
#
|
|
59
|
+
# * +:percent+ - Increment progress by a specific percent amount
|
|
60
|
+
# * +:set_percent+ - Set progress to a specific percent
|
|
61
|
+
#
|
|
62
|
+
def tick(percent: 0.01, set_percent: nil)
|
|
63
|
+
raise ArgumentError, 'percent and set_percent cannot both be specified' if percent != 0.01 && set_percent
|
|
64
|
+
@percent_done += percent
|
|
65
|
+
@percent_done = set_percent if set_percent
|
|
66
|
+
@percent_done = [@percent_done, 1.0].min # Make sure we can't go above 1.0
|
|
67
|
+
|
|
68
|
+
print to_s
|
|
69
|
+
print CLI::UI::ANSI.previous_line + "\n"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Format the progress bar to be printed to terminal
|
|
73
|
+
#
|
|
74
|
+
def to_s
|
|
75
|
+
suffix = " #{(@percent_done * 100).floor}%".ljust(5)
|
|
76
|
+
workable_width = @max_width - Frame.prefix_width - suffix.size
|
|
77
|
+
filled = [(@percent_done * workable_width.to_f).ceil, 0].max
|
|
78
|
+
unfilled = [workable_width - filled, 0].max
|
|
79
|
+
|
|
80
|
+
CLI::UI.resolve_text [
|
|
81
|
+
FILLED_BAR + ' ' * filled,
|
|
82
|
+
UNFILLED_BAR + ' ' * unfilled,
|
|
83
|
+
CLI::UI::Color::RESET.code + suffix
|
|
84
|
+
].join
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
require 'cli/ui'
|
|
3
|
+
require 'readline'
|
|
4
|
+
|
|
5
|
+
module CLI
|
|
6
|
+
module UI
|
|
7
|
+
module Prompt
|
|
8
|
+
autoload :InteractiveOptions, 'cli/ui/prompt/interactive_options'
|
|
9
|
+
autoload :OptionsHandler, 'cli/ui/prompt/options_handler'
|
|
10
|
+
private_constant :InteractiveOptions, :OptionsHandler
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
# Ask a user a question with either free form answer or a set of answers (multiple choice)
|
|
14
|
+
# Can use arrows, y/n, numbers (1/2), and vim bindings to control multiple choice selection
|
|
15
|
+
# Do not use this method for yes/no questions. Use +confirm+
|
|
16
|
+
#
|
|
17
|
+
# * Handles free form answers (options are nil)
|
|
18
|
+
# * Handles default answers for free form text
|
|
19
|
+
# * Handles file auto completion for file input
|
|
20
|
+
# * Handles interactively choosing answers using +InteractiveOptions+
|
|
21
|
+
#
|
|
22
|
+
# https://user-images.githubusercontent.com/3074765/33799822-47f23302-dd01-11e7-82f3-9072a5a5f611.png
|
|
23
|
+
#
|
|
24
|
+
# ==== Attributes
|
|
25
|
+
#
|
|
26
|
+
# * +question+ - (required) The question to ask the user
|
|
27
|
+
#
|
|
28
|
+
# ==== Options
|
|
29
|
+
#
|
|
30
|
+
# * +:options+ - Options that the user may select from. Will use +InteractiveOptions+ to do so.
|
|
31
|
+
# * +:default+ - The default answer to the question (e.g. they just press enter and don't input anything)
|
|
32
|
+
# * +:is_file+ - Tells the input to use file auto-completion (tab completion)
|
|
33
|
+
# * +:allow_empty+ - Allows the answer to be empty
|
|
34
|
+
# * +:multiple+ - Allow multiple options to be selected
|
|
35
|
+
# * +:filter_ui+ - Enable option filtering (default: true)
|
|
36
|
+
# * +:select_ui+ - Enable long-form option selection (default: true)
|
|
37
|
+
#
|
|
38
|
+
# Note:
|
|
39
|
+
# * +:options+ or providing a +Block+ conflicts with +:default+ and +:is_file+, you cannot set options with either of these keywords
|
|
40
|
+
# * +:default+ conflicts with +:allow_empty:, you cannot set these together
|
|
41
|
+
# * +:options+ conflicts with providing a +Block+ , you may only set one
|
|
42
|
+
# * +:multiple+ can only be used with +:options+ or a +Block+; it is ignored, otherwise.
|
|
43
|
+
#
|
|
44
|
+
# ==== Block (optional)
|
|
45
|
+
#
|
|
46
|
+
# * A Proc that provides a +OptionsHandler+ and uses the public +:option+ method to add options and their
|
|
47
|
+
# respective handlers
|
|
48
|
+
#
|
|
49
|
+
# ==== Return Value
|
|
50
|
+
#
|
|
51
|
+
# * If a +Block+ was not provided, the selected option or response to the free form question will be returned
|
|
52
|
+
# * If a +Block+ was provided, the evaluted value of the +Block+ will be returned
|
|
53
|
+
#
|
|
54
|
+
# ==== Example Usage:
|
|
55
|
+
#
|
|
56
|
+
# Free form question
|
|
57
|
+
# CLI::UI::Prompt.ask('What color is the sky?')
|
|
58
|
+
#
|
|
59
|
+
# Free form question with a file answer
|
|
60
|
+
# CLI::UI::Prompt.ask('Where is your Gemfile located?', is_file: true)
|
|
61
|
+
#
|
|
62
|
+
# Free form question with a default answer
|
|
63
|
+
# CLI::UI::Prompt.ask('What color is the sky?', default: 'blue')
|
|
64
|
+
#
|
|
65
|
+
# Free form question when the answer can be empty
|
|
66
|
+
# CLI::UI::Prompt.ask('What is your opinion on this question?', allow_empty: true)
|
|
67
|
+
#
|
|
68
|
+
# Interactive (multiple choice) question
|
|
69
|
+
# CLI::UI::Prompt.ask('What kind of project is this?', options: %w(rails go ruby python))
|
|
70
|
+
#
|
|
71
|
+
# Interactive (multiple choice) question with defined handlers
|
|
72
|
+
# CLI::UI::Prompt.ask('What kind of project is this?') do |handler|
|
|
73
|
+
# handler.option('rails') { |selection| selection }
|
|
74
|
+
# handler.option('go') { |selection| selection }
|
|
75
|
+
# handler.option('ruby') { |selection| selection }
|
|
76
|
+
# handler.option('python') { |selection| selection }
|
|
77
|
+
# end
|
|
78
|
+
#
|
|
79
|
+
def ask(question, options: nil, default: nil, is_file: nil, allow_empty: true, multiple: false, filter_ui: true, select_ui: true, &options_proc)
|
|
80
|
+
if ((options || block_given?) && (default || is_file))
|
|
81
|
+
raise(ArgumentError, 'conflicting arguments: options provided with default or is_file')
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
if options || block_given?
|
|
85
|
+
ask_interactive(question, options, multiple: multiple, filter_ui: filter_ui, select_ui: select_ui, &options_proc)
|
|
86
|
+
else
|
|
87
|
+
ask_free_form(question, default, is_file, allow_empty)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Asks the user for a single-line answer, without displaying the characters while typing.
|
|
92
|
+
# Typically used for password prompts
|
|
93
|
+
#
|
|
94
|
+
# ==== Return Value
|
|
95
|
+
#
|
|
96
|
+
# The password, without a trailing newline.
|
|
97
|
+
# If the user simply presses "Enter" without typing any password, this will return an empty string.
|
|
98
|
+
def ask_password(question)
|
|
99
|
+
require 'io/console'
|
|
100
|
+
|
|
101
|
+
CLI::UI.with_frame_color(:blue) do
|
|
102
|
+
STDOUT.print(CLI::UI.fmt('{{?}} ' + question)) # Do not use puts_question to avoid the new line.
|
|
103
|
+
|
|
104
|
+
# noecho interacts poorly with Readline under system Ruby, so do a manual `gets` here.
|
|
105
|
+
# No fancy Readline integration (like echoing back) is required for a password prompt anyway.
|
|
106
|
+
password = STDIN.noecho do
|
|
107
|
+
# Chomp will remove the one new line character added by `gets`, without touching potential extra spaces:
|
|
108
|
+
# " 123 \n".chomp => " 123 "
|
|
109
|
+
STDIN.gets.chomp
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
STDOUT.puts # Complete the line
|
|
113
|
+
|
|
114
|
+
password
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Asks the user a yes/no question.
|
|
119
|
+
# Can use arrows, y/n, numbers (1/2), and vim bindings to control
|
|
120
|
+
#
|
|
121
|
+
# ==== Example Usage:
|
|
122
|
+
#
|
|
123
|
+
# Confirmation question
|
|
124
|
+
# CLI::UI::Prompt.confirm('Is the sky blue?')
|
|
125
|
+
#
|
|
126
|
+
# CLI::UI::Prompt.confirm('Do a dangerous thing?', default: false)
|
|
127
|
+
#
|
|
128
|
+
def confirm(question, default: true)
|
|
129
|
+
ask_interactive(question, default ? %w(yes no) : %w(no yes), filter_ui: false) == 'yes'
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
def ask_free_form(question, default, is_file, allow_empty)
|
|
135
|
+
raise(ArgumentError, 'conflicting arguments: default enabled but allow_empty is false') if (default && !allow_empty)
|
|
136
|
+
|
|
137
|
+
if default
|
|
138
|
+
puts_question("#{question} (empty = #{default})")
|
|
139
|
+
else
|
|
140
|
+
puts_question(question)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Ask a free form question
|
|
144
|
+
loop do
|
|
145
|
+
line = readline(is_file: is_file)
|
|
146
|
+
|
|
147
|
+
if line.empty? && default
|
|
148
|
+
write_default_over_empty_input(default)
|
|
149
|
+
return default
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
if !line.empty? || allow_empty
|
|
153
|
+
return line
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def ask_interactive(question, options = nil, multiple: false, filter_ui: true, select_ui: true)
|
|
159
|
+
raise(ArgumentError, 'conflicting arguments: options and block given') if options && block_given?
|
|
160
|
+
|
|
161
|
+
options ||= if block_given?
|
|
162
|
+
handler = OptionsHandler.new
|
|
163
|
+
yield handler
|
|
164
|
+
handler.options
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
raise(ArgumentError, 'insufficient options') if options.nil? || options.empty?
|
|
168
|
+
instructions = (multiple ? "Toggle options. " : "") + "Choose with ↑ ↓ ⏎"
|
|
169
|
+
instructions += ", filter with 'f'" if filter_ui
|
|
170
|
+
instructions += ", enter option with 'e'" if select_ui and options.size > 9
|
|
171
|
+
puts_question("#{question} {{yellow:(#{instructions})}}")
|
|
172
|
+
resp = interactive_prompt(options, multiple: multiple)
|
|
173
|
+
|
|
174
|
+
# Clear the line
|
|
175
|
+
print ANSI.previous_line + ANSI.clear_to_end_of_line
|
|
176
|
+
# Force StdoutRouter to prefix
|
|
177
|
+
print ANSI.previous_line + "\n"
|
|
178
|
+
|
|
179
|
+
# reset the question to include the answer
|
|
180
|
+
resp_text = resp
|
|
181
|
+
if multiple
|
|
182
|
+
resp_text = case resp.size
|
|
183
|
+
when 0
|
|
184
|
+
"<nothing>"
|
|
185
|
+
when 1..2
|
|
186
|
+
resp.join(" and ")
|
|
187
|
+
else
|
|
188
|
+
"#{resp.size} items"
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
puts_question("#{question} (You chose: {{italic:#{resp_text}}})")
|
|
192
|
+
|
|
193
|
+
return handler.call(resp) if block_given?
|
|
194
|
+
resp
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Useful for stubbing in tests
|
|
198
|
+
def interactive_prompt(options, multiple: false)
|
|
199
|
+
InteractiveOptions.call(options, multiple: multiple)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def write_default_over_empty_input(default)
|
|
203
|
+
CLI::UI.raw do
|
|
204
|
+
STDERR.puts(
|
|
205
|
+
CLI::UI::ANSI.cursor_up(1) +
|
|
206
|
+
"\r" +
|
|
207
|
+
CLI::UI::ANSI.cursor_forward(4) + # TODO: width
|
|
208
|
+
default +
|
|
209
|
+
CLI::UI::Color::RESET.code
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def puts_question(str)
|
|
215
|
+
CLI::UI.with_frame_color(:blue) do
|
|
216
|
+
STDOUT.puts(CLI::UI.fmt('{{?}} ' + str))
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def readline(is_file: false)
|
|
221
|
+
if is_file
|
|
222
|
+
Readline.completion_proc = Readline::FILENAME_COMPLETION_PROC
|
|
223
|
+
Readline.completion_append_character = ""
|
|
224
|
+
else
|
|
225
|
+
Readline.completion_proc = proc { |*| nil }
|
|
226
|
+
Readline.completion_append_character = " "
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# because Readline is a C library, CLI::UI's hooks into $stdout don't
|
|
230
|
+
# work. We could work around this by having CLI::UI use a pipe and a
|
|
231
|
+
# thread to manage output, but the current strategy feels like a
|
|
232
|
+
# better tradeoff.
|
|
233
|
+
prefix = CLI::UI.with_frame_color(:blue) { CLI::UI::Frame.prefix }
|
|
234
|
+
prompt = prefix + CLI::UI.fmt('{{blue:> }}') + CLI::UI::Color::YELLOW.code
|
|
235
|
+
|
|
236
|
+
begin
|
|
237
|
+
line = Readline.readline(prompt, true)
|
|
238
|
+
print CLI::UI::Color::RESET.code
|
|
239
|
+
line.to_s.chomp
|
|
240
|
+
rescue Interrupt
|
|
241
|
+
CLI::UI.raw { STDERR.puts('^C' + CLI::UI::Color::RESET.code) }
|
|
242
|
+
raise
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
require 'io/console'
|
|
3
|
+
|
|
4
|
+
module CLI
|
|
5
|
+
module UI
|
|
6
|
+
module Prompt
|
|
7
|
+
class InteractiveOptions
|
|
8
|
+
DONE = "Done"
|
|
9
|
+
CHECKBOX_ICON = { false => "☐", true => "☑" }
|
|
10
|
+
|
|
11
|
+
# Prompts the user with options
|
|
12
|
+
# Uses an interactive session to allow the user to pick an answer
|
|
13
|
+
# Can use arrows, y/n, numbers (1/2), and vim bindings to control
|
|
14
|
+
# For more than 9 options, hitting 'e', ':', or 'G' will enter select
|
|
15
|
+
# mode allowing the user to type in longer numbers
|
|
16
|
+
# Pressing 'f' or '/' will allow the user to filter the results
|
|
17
|
+
#
|
|
18
|
+
# https://user-images.githubusercontent.com/3074765/33797984-0ebb5e64-dcdf-11e7-9e7e-7204f279cece.gif
|
|
19
|
+
#
|
|
20
|
+
# ==== Example Usage:
|
|
21
|
+
#
|
|
22
|
+
# Ask an interactive question
|
|
23
|
+
# CLI::UI::Prompt::InteractiveOptions.call(%w(rails go python))
|
|
24
|
+
#
|
|
25
|
+
def self.call(options, multiple: false)
|
|
26
|
+
list = new(options, multiple: multiple)
|
|
27
|
+
selected = list.call
|
|
28
|
+
if multiple
|
|
29
|
+
selected.map { |s| options[s - 1] }
|
|
30
|
+
else
|
|
31
|
+
options[selected - 1]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Initializes a new +InteractiveOptions+
|
|
36
|
+
# Usually called from +self.call+
|
|
37
|
+
#
|
|
38
|
+
# ==== Example Usage:
|
|
39
|
+
#
|
|
40
|
+
# CLI::UI::Prompt::InteractiveOptions.new(%w(rails go python))
|
|
41
|
+
#
|
|
42
|
+
def initialize(options, multiple: false)
|
|
43
|
+
@options = options
|
|
44
|
+
@active = 1
|
|
45
|
+
@marker = '>'
|
|
46
|
+
@answer = nil
|
|
47
|
+
@state = :root
|
|
48
|
+
@multiple = multiple
|
|
49
|
+
# Indicate that an extra line (the "metadata" line) is present and
|
|
50
|
+
# the terminal output should be drawn over when processing user input
|
|
51
|
+
@displaying_metadata = false
|
|
52
|
+
@filter = ''
|
|
53
|
+
# 0-indexed array representing if selected
|
|
54
|
+
# @options[0] is selected if @chosen[0]
|
|
55
|
+
@chosen = Array.new(@options.size) { false } if multiple
|
|
56
|
+
@redraw = true
|
|
57
|
+
@presented_options = []
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Calls the +InteractiveOptions+ and asks the question
|
|
61
|
+
# Usually used from +self.call+
|
|
62
|
+
#
|
|
63
|
+
def call
|
|
64
|
+
calculate_option_line_lengths
|
|
65
|
+
CLI::UI.raw { print(ANSI.hide_cursor) }
|
|
66
|
+
while @answer.nil?
|
|
67
|
+
render_options
|
|
68
|
+
process_input_until_redraw_required
|
|
69
|
+
reset_position
|
|
70
|
+
end
|
|
71
|
+
clear_output
|
|
72
|
+
|
|
73
|
+
@answer
|
|
74
|
+
ensure
|
|
75
|
+
CLI::UI.raw do
|
|
76
|
+
print(ANSI.show_cursor)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def calculate_option_line_lengths
|
|
83
|
+
@terminal_width_at_calculation_time = CLI::UI::Terminal.width
|
|
84
|
+
# options will be an array of questions but each option can be multi-line
|
|
85
|
+
# so to get the # of lines, you need to join then split
|
|
86
|
+
|
|
87
|
+
# since lines may be longer than the terminal is wide, we need to
|
|
88
|
+
# determine how many extra lines would be taken up by them
|
|
89
|
+
max_width = (@terminal_width_at_calculation_time -
|
|
90
|
+
@options.count.to_s.size - # Width of the displayed number
|
|
91
|
+
5 - # Extra characters added during rendering
|
|
92
|
+
(@multiple ? 1 : 0) # Space for the checkbox, if rendered
|
|
93
|
+
).to_f
|
|
94
|
+
|
|
95
|
+
@option_lengths = @options.map do |text|
|
|
96
|
+
width = 1 if text.empty?
|
|
97
|
+
width ||= text
|
|
98
|
+
.split("\n")
|
|
99
|
+
.reject(&:empty?)
|
|
100
|
+
.map { |l| (l.length / max_width).ceil }
|
|
101
|
+
.reduce(&:+)
|
|
102
|
+
|
|
103
|
+
width
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def reset_position(number_of_lines=num_lines)
|
|
108
|
+
# This will put us back at the beginning of the options
|
|
109
|
+
# When we redraw the options, they will be overwritten
|
|
110
|
+
CLI::UI.raw do
|
|
111
|
+
number_of_lines.times { print(ANSI.previous_line) }
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def clear_output(number_of_lines=num_lines)
|
|
116
|
+
CLI::UI.raw do
|
|
117
|
+
# Write over all lines with whitespace
|
|
118
|
+
number_of_lines.times { puts(' ' * CLI::UI::Terminal.width) }
|
|
119
|
+
end
|
|
120
|
+
reset_position number_of_lines
|
|
121
|
+
|
|
122
|
+
# Update if metadata is being displayed
|
|
123
|
+
# This must be done _after_ the output is cleared or it won't draw over
|
|
124
|
+
# the entire output
|
|
125
|
+
@displaying_metadata = display_metadata?
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Don't use this in place of +@displaying_metadata+, this updates too
|
|
129
|
+
# quickly to be useful when drawing to the screen.
|
|
130
|
+
def display_metadata?
|
|
131
|
+
filtering? or selecting? or has_filter?
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def num_lines
|
|
135
|
+
calculate_option_line_lengths if terminal_width_changed?
|
|
136
|
+
|
|
137
|
+
option_length = presented_options.reduce(0) do |total_length, (_, option_number)|
|
|
138
|
+
# Handle continuation markers and "Done" option when multiple is true
|
|
139
|
+
next total_length + 1 if option_number.nil? or option_number.zero?
|
|
140
|
+
total_length + @option_lengths[option_number - 1]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
option_length + (@displaying_metadata ? 1 : 0)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def terminal_width_changed?
|
|
147
|
+
@terminal_width_at_calculation_time != CLI::UI::Terminal.width
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
ESC = "\e"
|
|
151
|
+
BACKSPACE = "\u007F"
|
|
152
|
+
CTRL_C = "\u0003"
|
|
153
|
+
CTRL_D = "\u0004"
|
|
154
|
+
|
|
155
|
+
def up
|
|
156
|
+
active_index = @filtered_options.index { |_,num| num == @active } || 0
|
|
157
|
+
|
|
158
|
+
previous_visible = @filtered_options[active_index - 1]
|
|
159
|
+
previous_visible ||= @filtered_options.last
|
|
160
|
+
|
|
161
|
+
@active = previous_visible ? previous_visible.last : -1
|
|
162
|
+
@redraw = true
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def down
|
|
166
|
+
active_index = @filtered_options.index { |_,num| num == @active } || 0
|
|
167
|
+
|
|
168
|
+
next_visible = @filtered_options[active_index + 1]
|
|
169
|
+
next_visible ||= @filtered_options.first
|
|
170
|
+
|
|
171
|
+
@active = next_visible ? next_visible.last : -1
|
|
172
|
+
@redraw = true
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# n is 1-indexed selection
|
|
176
|
+
# n == 0 if "Done" was selected in @multiple mode
|
|
177
|
+
def select_n(n)
|
|
178
|
+
if @multiple
|
|
179
|
+
if n == 0
|
|
180
|
+
@answer = []
|
|
181
|
+
@chosen.each_with_index do |selected, i|
|
|
182
|
+
@answer << i + 1 if selected
|
|
183
|
+
end
|
|
184
|
+
else
|
|
185
|
+
@active = n
|
|
186
|
+
@chosen[n - 1] = !@chosen[n - 1]
|
|
187
|
+
end
|
|
188
|
+
elsif n == 0
|
|
189
|
+
# Ignore pressing "0" when not in multiple mode
|
|
190
|
+
else
|
|
191
|
+
@active = n
|
|
192
|
+
@answer = n
|
|
193
|
+
end
|
|
194
|
+
@redraw = true
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def select_bool(char)
|
|
198
|
+
return unless (@options - %w(yes no)).empty?
|
|
199
|
+
opt = @options.detect { |o| o.start_with?(char) }
|
|
200
|
+
@active = @options.index(opt) + 1
|
|
201
|
+
@answer = @options.index(opt) + 1
|
|
202
|
+
@redraw = true
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def build_selection(char)
|
|
206
|
+
@active = (@active.to_s + char).to_i
|
|
207
|
+
@redraw = true
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def chop_selection
|
|
211
|
+
@active = @active.to_s.chop.to_i
|
|
212
|
+
@redraw = true
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def update_search(char)
|
|
216
|
+
@redraw = true
|
|
217
|
+
|
|
218
|
+
# Control+D or Backspace on empty search closes search
|
|
219
|
+
if char == CTRL_D or (@filter.empty? and char == BACKSPACE)
|
|
220
|
+
@filter = ''
|
|
221
|
+
@state = :root
|
|
222
|
+
return
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
if char == BACKSPACE
|
|
226
|
+
@filter.chop!
|
|
227
|
+
else
|
|
228
|
+
@filter += char
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def select_current
|
|
233
|
+
# Prevent selection of invisible options
|
|
234
|
+
return unless presented_options.any? { |_,num| num == @active }
|
|
235
|
+
select_n(@active)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def process_input_until_redraw_required
|
|
239
|
+
@redraw = false
|
|
240
|
+
wait_for_user_input until @redraw
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# rubocop:disable Style/WhenThen,Layout/SpaceBeforeSemicolon
|
|
244
|
+
def wait_for_user_input
|
|
245
|
+
char = read_char
|
|
246
|
+
@last_char = char
|
|
247
|
+
|
|
248
|
+
case char
|
|
249
|
+
when :timeout ; raise Interrupt # Timeout, use interrupt to simulate
|
|
250
|
+
when CTRL_C ; raise Interrupt
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
case @state
|
|
254
|
+
when :root
|
|
255
|
+
case char
|
|
256
|
+
when ESC ; @state = :esc
|
|
257
|
+
when 'k' ; up
|
|
258
|
+
when 'j' ; down
|
|
259
|
+
when 'e', ':', 'G' ; start_line_select
|
|
260
|
+
when 'f', '/' ; start_filter
|
|
261
|
+
when ('0'..@options.size.to_s) ; select_n(char.to_i)
|
|
262
|
+
when 'y', 'n' ; select_bool(char)
|
|
263
|
+
when " ", "\r", "\n" ; select_current # <enter>
|
|
264
|
+
end
|
|
265
|
+
when :filter
|
|
266
|
+
case char
|
|
267
|
+
when ESC ; @state = :esc
|
|
268
|
+
when "\r", "\n" ; select_current
|
|
269
|
+
else ; update_search(char)
|
|
270
|
+
end
|
|
271
|
+
when :line_select
|
|
272
|
+
case char
|
|
273
|
+
when ESC ; @state = :esc
|
|
274
|
+
when 'k' ; up ; @state = :root
|
|
275
|
+
when 'j' ; down ; @state = :root
|
|
276
|
+
when 'e',':','G','q' ; stop_line_select
|
|
277
|
+
when '0'..'9' ; build_selection(char)
|
|
278
|
+
when BACKSPACE ; chop_selection # Pop last input on backspace
|
|
279
|
+
when ' ', "\r", "\n" ; select_current
|
|
280
|
+
end
|
|
281
|
+
when :esc
|
|
282
|
+
case char
|
|
283
|
+
when '[' ; @state = :esc_bracket
|
|
284
|
+
else ; raise Interrupt # unhandled escape sequence.
|
|
285
|
+
end
|
|
286
|
+
when :esc_bracket
|
|
287
|
+
@state = has_filter? ? :filter : :root
|
|
288
|
+
case char
|
|
289
|
+
when 'A' ; up
|
|
290
|
+
when 'B' ; down
|
|
291
|
+
when 'C' ; # Ignore right key
|
|
292
|
+
when 'D' ; # Ignore left key
|
|
293
|
+
else ; raise Interrupt # unhandled escape sequence.
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
# rubocop:enable Style/WhenThen,Layout/SpaceBeforeSemicolon
|
|
298
|
+
|
|
299
|
+
def selecting?
|
|
300
|
+
@state == :line_select
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def filtering?
|
|
304
|
+
@state == :filter
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def has_filter?
|
|
308
|
+
!@filter.empty?
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def start_filter
|
|
312
|
+
@state = :filter
|
|
313
|
+
@redraw = true
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def start_line_select
|
|
317
|
+
@state = :line_select
|
|
318
|
+
@active = 0
|
|
319
|
+
@redraw = true
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def stop_line_select
|
|
323
|
+
@state = :root
|
|
324
|
+
@active = 1 if @active.zero?
|
|
325
|
+
@redraw = true
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def read_char
|
|
329
|
+
raw_tty! do
|
|
330
|
+
getc = $stdin.getc
|
|
331
|
+
getc ? getc.chr : :timeout
|
|
332
|
+
end
|
|
333
|
+
rescue IOError
|
|
334
|
+
"\e"
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def raw_tty!
|
|
338
|
+
if ENV['TEST'] || !$stdin.tty?
|
|
339
|
+
yield
|
|
340
|
+
else
|
|
341
|
+
$stdin.raw { yield }
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def presented_options(recalculate: false)
|
|
346
|
+
return @presented_options unless recalculate
|
|
347
|
+
|
|
348
|
+
@presented_options = @options.zip(1..Float::INFINITY)
|
|
349
|
+
if has_filter?
|
|
350
|
+
@presented_options.select! { |option,_| option.downcase.include?(@filter.downcase) }
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Used for selection purposes
|
|
354
|
+
@filtered_options = @presented_options.dup
|
|
355
|
+
|
|
356
|
+
@presented_options.unshift([DONE, 0]) if @multiple
|
|
357
|
+
|
|
358
|
+
ensure_visible_is_active if has_filter?
|
|
359
|
+
|
|
360
|
+
while num_lines > max_lines
|
|
361
|
+
# try to keep the selection centered in the window:
|
|
362
|
+
if distance_from_selection_to_end > distance_from_start_to_selection
|
|
363
|
+
# selection is closer to top than bottom, so trim a row from the bottom
|
|
364
|
+
ensure_last_item_is_continuation_marker
|
|
365
|
+
@presented_options.delete_at(-2)
|
|
366
|
+
else
|
|
367
|
+
# selection is closer to bottom than top, so trim a row from the top
|
|
368
|
+
ensure_first_item_is_continuation_marker
|
|
369
|
+
@presented_options.delete_at(1)
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
@presented_options
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def ensure_visible_is_active
|
|
377
|
+
unless presented_options.any? { |_, num| num == @active }
|
|
378
|
+
@active = presented_options.first&.last.to_i
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def distance_from_selection_to_end
|
|
383
|
+
@presented_options.count - index_of_active_option
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def distance_from_start_to_selection
|
|
387
|
+
index_of_active_option
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def index_of_active_option
|
|
391
|
+
@presented_options.index { |_,num| num == @active }.to_i
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def ensure_last_item_is_continuation_marker
|
|
395
|
+
@presented_options.push(["...", nil]) if @presented_options.last.last
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def ensure_first_item_is_continuation_marker
|
|
399
|
+
@presented_options.unshift(["...", nil]) if @presented_options.first.last
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def max_lines
|
|
403
|
+
CLI::UI::Terminal.height - (@displaying_metadata ? 3 : 2) # Keeps a one line question visible
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def render_options
|
|
407
|
+
previously_displayed_lines = num_lines
|
|
408
|
+
|
|
409
|
+
@displaying_metadata = display_metadata?
|
|
410
|
+
|
|
411
|
+
options = presented_options(recalculate: true)
|
|
412
|
+
|
|
413
|
+
clear_output(previously_displayed_lines) if previously_displayed_lines > num_lines
|
|
414
|
+
|
|
415
|
+
max_num_length = (@options.size + 1).to_s.length
|
|
416
|
+
|
|
417
|
+
metadata_text = if selecting?
|
|
418
|
+
select_text = @active
|
|
419
|
+
select_text = '{{info:e, q, or up/down anytime to exit}}' if @active == 0
|
|
420
|
+
"Select: #{select_text}"
|
|
421
|
+
elsif filtering? or has_filter?
|
|
422
|
+
filter_text = @filter
|
|
423
|
+
filter_text = '{{info:Ctrl-D anytime or Backspace now to exit}}' if @filter.empty?
|
|
424
|
+
"Filter: #{filter_text}"
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
if metadata_text
|
|
428
|
+
CLI::UI.with_frame_color(:blue) do
|
|
429
|
+
puts CLI::UI.fmt(" {{green:#{metadata_text}}}#{ANSI.clear_to_end_of_line}")
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
options.each do |choice, num|
|
|
434
|
+
is_chosen = @multiple && num && @chosen[num - 1]
|
|
435
|
+
|
|
436
|
+
padding = ' ' * (max_num_length - num.to_s.length)
|
|
437
|
+
message = " #{num}#{num ? '.' : ' '}#{padding}"
|
|
438
|
+
|
|
439
|
+
format = "%s"
|
|
440
|
+
# If multiple, bold only selected. If not multiple, bold everything
|
|
441
|
+
format = "{{bold:#{format}}}" if !@multiple || is_chosen
|
|
442
|
+
format = "{{cyan:#{format}}}" if @multiple && is_chosen && num != @active
|
|
443
|
+
format = " #{format}"
|
|
444
|
+
|
|
445
|
+
message += sprintf(format, CHECKBOX_ICON[is_chosen]) if @multiple && num && num > 0
|
|
446
|
+
message += format_choice(format, choice)
|
|
447
|
+
|
|
448
|
+
if num == @active
|
|
449
|
+
|
|
450
|
+
color = (filtering? or selecting?) ? 'green' : 'blue'
|
|
451
|
+
message = message.split("\n").map { |l| "{{#{color}:> #{l.strip}}}" }.join("\n")
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
CLI::UI.with_frame_color(:blue) do
|
|
455
|
+
puts CLI::UI.fmt(message)
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def format_choice(format, choice)
|
|
461
|
+
eol = CLI::UI::ANSI.clear_to_end_of_line
|
|
462
|
+
lines = choice.split("\n")
|
|
463
|
+
|
|
464
|
+
return eol if lines.empty? # Handle blank options
|
|
465
|
+
|
|
466
|
+
lines.map! { |l| sprintf(format, l) + eol }
|
|
467
|
+
lines.join("\n")
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
end
|