shopify-cli 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|