rfix 2.0.4 → 3.0.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.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/exe/rfix +11 -90
  3. data/lib/rfix.rb +10 -9
  4. data/lib/rfix/branch/reference.rb +2 -2
  5. data/lib/rfix/branch/upstream.rb +2 -4
  6. data/lib/rfix/cli/command.rb +14 -1
  7. data/lib/rfix/cli/command/all.rb +21 -0
  8. data/lib/rfix/cli/command/base.rb +30 -19
  9. data/lib/rfix/cli/command/branch.rb +2 -0
  10. data/lib/rfix/cli/command/help.rb +2 -0
  11. data/lib/rfix/cli/command/info.rb +6 -1
  12. data/lib/rfix/cli/command/local.rb +2 -0
  13. data/lib/rfix/cli/command/origin.rb +2 -0
  14. data/lib/rfix/cli/command/setup.rb +2 -0
  15. data/lib/rfix/cli/command/status.rb +39 -0
  16. data/lib/rfix/collector.rb +69 -0
  17. data/lib/rfix/diff.rb +69 -0
  18. data/lib/rfix/extension/comment_config.rb +15 -0
  19. data/lib/rfix/extension/offense.rb +17 -14
  20. data/lib/rfix/extension/pastel.rb +7 -4
  21. data/lib/rfix/extension/progresbar.rb +15 -0
  22. data/lib/rfix/extension/strings.rb +10 -2
  23. data/lib/rfix/file.rb +5 -3
  24. data/lib/rfix/file/base.rb +21 -14
  25. data/lib/rfix/file/deleted.rb +2 -0
  26. data/lib/rfix/file/ignored.rb +2 -0
  27. data/lib/rfix/file/null.rb +17 -0
  28. data/lib/rfix/file/tracked.rb +39 -23
  29. data/lib/rfix/file/undefined.rb +17 -0
  30. data/lib/rfix/file/untracked.rb +3 -1
  31. data/lib/rfix/formatter.rb +67 -71
  32. data/lib/rfix/highlighter.rb +1 -3
  33. data/lib/rfix/rake/gemfile.rb +26 -23
  34. data/lib/rfix/repository.rb +59 -96
  35. data/lib/rfix/types.rb +24 -14
  36. data/lib/rfix/version.rb +1 -1
  37. data/rfix.gemspec +11 -3
  38. data/vendor/cli-ui/Gemfile +17 -0
  39. data/vendor/cli-ui/Gemfile.lock +60 -0
  40. data/vendor/cli-ui/LICENSE.txt +21 -0
  41. data/vendor/cli-ui/README.md +224 -0
  42. data/vendor/cli-ui/Rakefile +20 -0
  43. data/vendor/cli-ui/bin/console +14 -0
  44. data/vendor/cli-ui/cli-ui.gemspec +25 -0
  45. data/vendor/cli-ui/dev.yml +14 -0
  46. data/vendor/cli-ui/lib/cli/ui.rb +233 -0
  47. data/vendor/cli-ui/lib/cli/ui/ansi.rb +157 -0
  48. data/vendor/cli-ui/lib/cli/ui/color.rb +84 -0
  49. data/vendor/cli-ui/lib/cli/ui/formatter.rb +192 -0
  50. data/vendor/cli-ui/lib/cli/ui/frame.rb +269 -0
  51. data/vendor/cli-ui/lib/cli/ui/frame/frame_stack.rb +98 -0
  52. data/vendor/cli-ui/lib/cli/ui/frame/frame_style.rb +120 -0
  53. data/vendor/cli-ui/lib/cli/ui/frame/frame_style/box.rb +166 -0
  54. data/vendor/cli-ui/lib/cli/ui/frame/frame_style/bracket.rb +139 -0
  55. data/vendor/cli-ui/lib/cli/ui/glyph.rb +84 -0
  56. data/vendor/cli-ui/lib/cli/ui/os.rb +67 -0
  57. data/vendor/cli-ui/lib/cli/ui/printer.rb +59 -0
  58. data/vendor/cli-ui/lib/cli/ui/progress.rb +90 -0
  59. data/vendor/cli-ui/lib/cli/ui/prompt.rb +297 -0
  60. data/vendor/cli-ui/lib/cli/ui/prompt/interactive_options.rb +484 -0
  61. data/vendor/cli-ui/lib/cli/ui/prompt/options_handler.rb +29 -0
  62. data/vendor/cli-ui/lib/cli/ui/spinner.rb +66 -0
  63. data/vendor/cli-ui/lib/cli/ui/spinner/async.rb +40 -0
  64. data/vendor/cli-ui/lib/cli/ui/spinner/spin_group.rb +263 -0
  65. data/vendor/cli-ui/lib/cli/ui/stdout_router.rb +232 -0
  66. data/vendor/cli-ui/lib/cli/ui/terminal.rb +46 -0
  67. data/vendor/cli-ui/lib/cli/ui/truncater.rb +102 -0
  68. data/vendor/cli-ui/lib/cli/ui/version.rb +5 -0
  69. data/vendor/cli-ui/lib/cli/ui/widgets.rb +77 -0
  70. data/vendor/cli-ui/lib/cli/ui/widgets/base.rb +27 -0
  71. data/vendor/cli-ui/lib/cli/ui/widgets/status.rb +61 -0
  72. data/vendor/cli-ui/lib/cli/ui/wrap.rb +56 -0
  73. data/vendor/cli-ui/test/cli/ui/ansi_test.rb +32 -0
  74. data/vendor/cli-ui/test/cli/ui/cli_ui_test.rb +23 -0
  75. data/vendor/cli-ui/test/cli/ui/color_test.rb +40 -0
  76. data/vendor/cli-ui/test/cli/ui/formatter_test.rb +79 -0
  77. data/vendor/cli-ui/test/cli/ui/glyph_test.rb +68 -0
  78. data/vendor/cli-ui/test/cli/ui/printer_test.rb +103 -0
  79. data/vendor/cli-ui/test/cli/ui/progress_test.rb +46 -0
  80. data/vendor/cli-ui/test/cli/ui/prompt/options_handler_test.rb +39 -0
  81. data/vendor/cli-ui/test/cli/ui/prompt_test.rb +348 -0
  82. data/vendor/cli-ui/test/cli/ui/spinner/spin_group_test.rb +39 -0
  83. data/vendor/cli-ui/test/cli/ui/spinner_test.rb +141 -0
  84. data/vendor/cli-ui/test/cli/ui/stdout_router_test.rb +32 -0
  85. data/vendor/cli-ui/test/cli/ui/terminal_test.rb +26 -0
  86. data/vendor/cli-ui/test/cli/ui/truncater_test.rb +31 -0
  87. data/vendor/cli-ui/test/cli/ui/widgets/status_test.rb +49 -0
  88. data/vendor/cli-ui/test/cli/ui/widgets_test.rb +15 -0
  89. data/vendor/cli-ui/test/test_helper.rb +53 -0
  90. data/vendor/cli-ui/tmp/cache/bootsnap/compile-cache/d9/c036af0f3dc494 +0 -0
  91. data/vendor/cli-ui/tmp/cache/bootsnap/load-path-cache +0 -0
  92. data/vendor/dry-cli/lib/dry/cli/command.rb +2 -1
  93. data/vendor/dry-cli/tmp/cache/bootsnap/compile-cache/ff/a22a5daafbd74c +0 -0
  94. data/vendor/dry-cli/tmp/cache/bootsnap/load-path-cache +0 -0
  95. data/vendor/strings-ansi/tmp/cache/bootsnap/compile-cache/79/49cf49407b370e +0 -0
  96. data/vendor/strings-ansi/tmp/cache/bootsnap/load-path-cache +0 -0
  97. metadata +170 -9
  98. data/lib/rfix/extension/string.rb +0 -12
  99. data/lib/rfix/indicator.rb +0 -19
@@ -0,0 +1,84 @@
1
+ require 'cli/ui'
2
+
3
+ module CLI
4
+ module UI
5
+ class Glyph
6
+ class InvalidGlyphHandle < ArgumentError
7
+ def initialize(handle)
8
+ super
9
+ @handle = handle
10
+ end
11
+
12
+ def message
13
+ keys = Glyph.available.join(',')
14
+ "invalid glyph handle: #{@handle} " \
15
+ "-- must be one of CLI::UI::Glyph.available (#{keys})"
16
+ end
17
+ end
18
+
19
+ attr_reader :handle, :codepoint, :color, :to_s, :fmt
20
+
21
+ # Creates a new glyph
22
+ #
23
+ # ==== Attributes
24
+ #
25
+ # * +handle+ - The handle in the +MAP+ constant
26
+ # * +codepoint+ - The codepoint used to create the glyph (e.g. +0x2717+ for a ballot X)
27
+ # * +plain+ - A fallback plain string to be used in case glyphs are disabled
28
+ # * +color+ - What color to output the glyph. Check +CLI::UI::Color+ for options.
29
+ #
30
+ def initialize(handle, codepoint, plain, color)
31
+ @handle = handle
32
+ @codepoint = codepoint
33
+ @color = color
34
+ @plain = plain
35
+ @char = Array(codepoint).pack('U*')
36
+ @to_s = color.code + char + Color::RESET.code
37
+ @fmt = "{{#{color.name}:#{char}}}"
38
+
39
+ MAP[handle] = self
40
+ end
41
+
42
+ # Fetches the actual character(s) to be displayed for a glyph, based on the current OS support
43
+ #
44
+ # ==== Returns
45
+ # Returns the glyph string
46
+ def char
47
+ CLI::UI::OS.current.supports_emoji? ? @char : @plain
48
+ end
49
+
50
+ # Mapping of glyphs to terminal output
51
+ MAP = {}
52
+ STAR = new('*', 0x2b51, '*', Color::YELLOW) # YELLOW SMALL STAR (⭑)
53
+ INFO = new('i', 0x1d4be, 'i', Color::BLUE) # BLUE MATHEMATICAL SCRIPT SMALL i (𝒾)
54
+ QUESTION = new('?', 0x003f, '?', Color::BLUE) # BLUE QUESTION MARK (?)
55
+ CHECK = new('v', 0x2713, '√', Color::GREEN) # GREEN CHECK MARK (✓)
56
+ X = new('x', 0x2717, 'X', Color::RED) # RED BALLOT X (✗)
57
+ BUG = new('b', 0x1f41b, '!', Color::WHITE) # Bug emoji (🐛)
58
+ CHEVRON = new('>', 0xbb, '»', Color::YELLOW) # RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK (»)
59
+ HOURGLASS = new('H', [0x231b, 0xfe0e], 'H', Color::BLUE) # HOURGLASS + VARIATION SELECTOR 15 (⌛︎)
60
+ WARNING = new('!', [0x26a0, 0xfe0f], '!', Color::YELLOW) # WARNING SIGN + VARIATION SELECTOR 16 (⚠️ )
61
+
62
+ # Looks up a glyph by name
63
+ #
64
+ # ==== Raises
65
+ # Raises a InvalidGlyphHandle if the glyph is not available
66
+ # You likely need to create it with +.new+ or you made a typo
67
+ #
68
+ # ==== Returns
69
+ # Returns a terminal output-capable string
70
+ #
71
+ def self.lookup(name)
72
+ MAP.fetch(name.to_s)
73
+ rescue KeyError
74
+ raise InvalidGlyphHandle, name
75
+ end
76
+
77
+ # All available glyphs by name
78
+ #
79
+ def self.available
80
+ MAP.keys
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,67 @@
1
+ require 'rbconfig'
2
+
3
+ module CLI
4
+ module UI
5
+ module OS
6
+ # Determines which OS is currently running the UI, to make it easier to
7
+ # adapt its behaviour to the features of the OS.
8
+ def self.current
9
+ @current_os ||= case RbConfig::CONFIG['host_os']
10
+ when /darwin/
11
+ Mac
12
+ when /linux/
13
+ Linux
14
+ else
15
+ if RUBY_PLATFORM !~ /cygwin/ && ENV['OS'] == 'Windows_NT'
16
+ Windows
17
+ else
18
+ raise "Could not determine OS from host_os #{RbConfig::CONFIG["host_os"]}"
19
+ end
20
+ end
21
+ end
22
+
23
+ class Mac
24
+ class << self
25
+ def supports_emoji?
26
+ true
27
+ end
28
+
29
+ def supports_color_prompt?
30
+ true
31
+ end
32
+
33
+ def supports_arrow_keys?
34
+ true
35
+ end
36
+
37
+ def shift_cursor_on_line_reset?
38
+ false
39
+ end
40
+ end
41
+ end
42
+
43
+ class Linux < Mac
44
+ end
45
+
46
+ class Windows
47
+ class << self
48
+ def supports_emoji?
49
+ false
50
+ end
51
+
52
+ def supports_color_prompt?
53
+ false
54
+ end
55
+
56
+ def supports_arrow_keys?
57
+ false
58
+ end
59
+
60
+ def shift_cursor_on_line_reset?
61
+ true
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,59 @@
1
+ require 'cli/ui'
2
+
3
+ module CLI
4
+ module UI
5
+ class Printer
6
+ # Print a message to a stream with common utilities.
7
+ # Allows overriding the color, encoding, and target stream.
8
+ # By default, it formats the string using CLI:UI and rescues common stream errors.
9
+ #
10
+ # ==== Attributes
11
+ #
12
+ # * +msg+ - (required) the string to output. Can be frozen.
13
+ #
14
+ # ==== Options
15
+ #
16
+ # * +:frame_color+ - Override the frame color. Defaults to nil.
17
+ # * +:to+ - Target stream, like $stdout or $stderr. Can be anything with a puts method. Defaults to $stdout.
18
+ # * +:encoding+ - Force the output to be in a certain encoding. Defaults to UTF-8.
19
+ # * +:format+ - Whether to format the string using CLI::UI.fmt. Defaults to true.
20
+ # * +:graceful+ - Whether to gracefully ignore common I/O errors. Defaults to true.
21
+ # * +:wrap+ - Whether to wrap text at word boundaries to terminal width. Defaults to true.
22
+ #
23
+ # ==== Returns
24
+ # Returns whether the message was successfully printed,
25
+ # which can be useful if +:graceful+ is set to true.
26
+ #
27
+ # ==== Example
28
+ #
29
+ # CLI::UI::Printer.puts('{{x}} Ouch', to: $stderr)
30
+ #
31
+ def self.puts(
32
+ msg,
33
+ frame_color:
34
+ nil,
35
+ to:
36
+ $stdout,
37
+ encoding: Encoding::UTF_8,
38
+ format: true,
39
+ graceful: true,
40
+ wrap: true
41
+ )
42
+ msg = (+msg).force_encoding(encoding) if encoding
43
+ msg = CLI::UI.fmt(msg) if format
44
+ msg = CLI::UI.wrap(msg) if wrap
45
+
46
+ if frame_color
47
+ CLI::UI::Frame.with_frame_color_override(frame_color) { to.puts(msg) }
48
+ else
49
+ to.puts(msg)
50
+ end
51
+
52
+ true
53
+ rescue Errno::EIO, Errno::EPIPE, IOError => e
54
+ raise(e) unless graceful
55
+ false
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,90 @@
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: 0.05)
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
+ # *Note:* The +:percent+ and +:set_percent must be between 0.00 and 1.0
63
+ #
64
+ def tick(percent: 0.01, set_percent: nil)
65
+ raise ArgumentError, 'percent and set_percent cannot both be specified' if percent != 0.01 && set_percent
66
+ @percent_done += percent
67
+ @percent_done = set_percent if set_percent
68
+ @percent_done = [@percent_done, 1.0].min # Make sure we can't go above 1.0
69
+
70
+ print(to_s)
71
+ print(CLI::UI::ANSI.previous_line + "\n")
72
+ end
73
+
74
+ # Format the progress bar to be printed to terminal
75
+ #
76
+ def to_s
77
+ suffix = " #{(@percent_done * 100).floor}%".ljust(5)
78
+ workable_width = @max_width - Frame.prefix_width - suffix.size
79
+ filled = [(@percent_done * workable_width.to_f).ceil, 0].max
80
+ unfilled = [workable_width - filled, 0].max
81
+
82
+ CLI::UI.resolve_text([
83
+ FILLED_BAR + ' ' * filled,
84
+ UNFILLED_BAR + ' ' * unfilled,
85
+ CLI::UI::Color::RESET.code + suffix,
86
+ ].join)
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,297 @@
1
+ # coding: utf-8
2
+ require 'cli/ui'
3
+ require 'readline'
4
+
5
+ module Readline
6
+ unless const_defined?(:FILENAME_COMPLETION_PROC)
7
+ FILENAME_COMPLETION_PROC = proc do |input|
8
+ directory = input[-1] == '/' ? input : File.dirname(input)
9
+ filename = input[-1] == '/' ? '' : File.basename(input)
10
+
11
+ (Dir.entries(directory).select do |fp|
12
+ fp.start_with?(filename)
13
+ end - (input[-1] == '.' ? [] : ['.', '..'])).map do |fp|
14
+ File.join(directory, fp).gsub(/\A\.\//, '')
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ module CLI
21
+ module UI
22
+ module Prompt
23
+ autoload :InteractiveOptions, 'cli/ui/prompt/interactive_options'
24
+ autoload :OptionsHandler, 'cli/ui/prompt/options_handler'
25
+ private_constant :InteractiveOptions, :OptionsHandler
26
+
27
+ class << self
28
+ # Ask a user a question with either free form answer or a set of answers (multiple choice)
29
+ # Can use arrows, y/n, numbers (1/2), and vim bindings to control multiple choice selection
30
+ # Do not use this method for yes/no questions. Use +confirm+
31
+ #
32
+ # * Handles free form answers (options are nil)
33
+ # * Handles default answers for free form text
34
+ # * Handles file auto completion for file input
35
+ # * Handles interactively choosing answers using +InteractiveOptions+
36
+ #
37
+ # https://user-images.githubusercontent.com/3074765/33799822-47f23302-dd01-11e7-82f3-9072a5a5f611.png
38
+ #
39
+ # ==== Attributes
40
+ #
41
+ # * +question+ - (required) The question to ask the user
42
+ #
43
+ # ==== Options
44
+ #
45
+ # * +:options+ - Options that the user may select from. Will use +InteractiveOptions+ to do so.
46
+ # * +:default+ - The default answer to the question (e.g. they just press enter and don't input anything)
47
+ # * +:is_file+ - Tells the input to use file auto-completion (tab completion)
48
+ # * +:allow_empty+ - Allows the answer to be empty
49
+ # * +:multiple+ - Allow multiple options to be selected
50
+ # * +:filter_ui+ - Enable option filtering (default: true)
51
+ # * +:select_ui+ - Enable long-form option selection (default: true)
52
+ #
53
+ # Note:
54
+ # * +:options+ or providing a +Block+ conflicts with +:default+ and +:is_file+,
55
+ # you cannot set options with either of these keywords
56
+ # * +:default+ conflicts with +:allow_empty:, you cannot set these together
57
+ # * +:options+ conflicts with providing a +Block+ , you may only set one
58
+ # * +:multiple+ can only be used with +:options+ or a +Block+; it is ignored, otherwise.
59
+ #
60
+ # ==== Block (optional)
61
+ #
62
+ # * A Proc that provides a +OptionsHandler+ and uses the public +:option+ method to add options and their
63
+ # respective handlers
64
+ #
65
+ # ==== Return Value
66
+ #
67
+ # * If a +Block+ was not provided, the selected option or response to the free form question will be returned
68
+ # * If a +Block+ was provided, the evaluated value of the +Block+ will be returned
69
+ #
70
+ # ==== Example Usage:
71
+ #
72
+ # Free form question
73
+ # CLI::UI::Prompt.ask('What color is the sky?')
74
+ #
75
+ # Free form question with a file answer
76
+ # CLI::UI::Prompt.ask('Where is your Gemfile located?', is_file: true)
77
+ #
78
+ # Free form question with a default answer
79
+ # CLI::UI::Prompt.ask('What color is the sky?', default: 'blue')
80
+ #
81
+ # Free form question when the answer can be empty
82
+ # CLI::UI::Prompt.ask('What is your opinion on this question?', allow_empty: true)
83
+ #
84
+ # Interactive (multiple choice) question
85
+ # CLI::UI::Prompt.ask('What kind of project is this?', options: %w(rails go ruby python))
86
+ #
87
+ # Interactive (multiple choice) question with defined handlers
88
+ # CLI::UI::Prompt.ask('What kind of project is this?') do |handler|
89
+ # handler.option('rails') { |selection| selection }
90
+ # handler.option('go') { |selection| selection }
91
+ # handler.option('ruby') { |selection| selection }
92
+ # handler.option('python') { |selection| selection }
93
+ # end
94
+ #
95
+ def ask(
96
+ question,
97
+ options: nil,
98
+ default: nil,
99
+ is_file: nil,
100
+ allow_empty: true,
101
+ multiple: false,
102
+ filter_ui: true,
103
+ select_ui: true,
104
+ &options_proc
105
+ )
106
+ if (options || block_given?) && ((default && !multiple) || is_file)
107
+ raise(ArgumentError, 'conflicting arguments: options provided with default or is_file')
108
+ end
109
+
110
+ if options && multiple && default && !(default - options).empty?
111
+ raise(ArgumentError, 'conflicting arguments: default should only include elements present in options')
112
+ end
113
+
114
+ if options || block_given?
115
+ ask_interactive(
116
+ question,
117
+ options,
118
+ multiple: multiple,
119
+ default: default,
120
+ filter_ui: filter_ui,
121
+ select_ui: select_ui,
122
+ &options_proc
123
+ )
124
+ else
125
+ ask_free_form(question, default, is_file, allow_empty)
126
+ end
127
+ end
128
+
129
+ # Asks the user for a single-line answer, without displaying the characters while typing.
130
+ # Typically used for password prompts
131
+ #
132
+ # ==== Return Value
133
+ #
134
+ # The password, without a trailing newline.
135
+ # If the user simply presses "Enter" without typing any password, this will return an empty string.
136
+ def ask_password(question)
137
+ require 'io/console'
138
+
139
+ CLI::UI.with_frame_color(:blue) do
140
+ STDOUT.print(CLI::UI.fmt('{{?}} ' + question)) # Do not use puts_question to avoid the new line.
141
+
142
+ # noecho interacts poorly with Readline under system Ruby, so do a manual `gets` here.
143
+ # No fancy Readline integration (like echoing back) is required for a password prompt anyway.
144
+ password = STDIN.noecho do
145
+ # Chomp will remove the one new line character added by `gets`, without touching potential extra spaces:
146
+ # " 123 \n".chomp => " 123 "
147
+ STDIN.gets.chomp
148
+ end
149
+
150
+ STDOUT.puts # Complete the line
151
+
152
+ password
153
+ end
154
+ end
155
+
156
+ # Asks the user a yes/no question.
157
+ # Can use arrows, y/n, numbers (1/2), and vim bindings to control
158
+ #
159
+ # ==== Example Usage:
160
+ #
161
+ # Confirmation question
162
+ # CLI::UI::Prompt.confirm('Is the sky blue?')
163
+ #
164
+ # CLI::UI::Prompt.confirm('Do a dangerous thing?', default: false)
165
+ #
166
+ def confirm(question, default: true)
167
+ ask_interactive(question, default ? %w(yes no) : %w(no yes), filter_ui: false) == 'yes'
168
+ end
169
+
170
+ private
171
+
172
+ def ask_free_form(question, default, is_file, allow_empty)
173
+ if default && !allow_empty
174
+ raise(ArgumentError, 'conflicting arguments: default enabled but allow_empty is false')
175
+ end
176
+
177
+ if default
178
+ puts_question("#{question} (empty = #{default})")
179
+ else
180
+ puts_question(question)
181
+ end
182
+
183
+ # Ask a free form question
184
+ loop do
185
+ line = readline(is_file: is_file)
186
+
187
+ if line.empty? && default
188
+ write_default_over_empty_input(default)
189
+ return default
190
+ end
191
+
192
+ if !line.empty? || allow_empty
193
+ return line
194
+ end
195
+ end
196
+ end
197
+
198
+ def ask_interactive(question, options = nil, multiple: false, default: nil, filter_ui: true, select_ui: true)
199
+ raise(ArgumentError, 'conflicting arguments: options and block given') if options && block_given?
200
+
201
+ options ||= if block_given?
202
+ handler = OptionsHandler.new
203
+ yield handler
204
+ handler.options
205
+ end
206
+
207
+ raise(ArgumentError, 'insufficient options') if options.nil? || options.empty?
208
+ navigate_text = if CLI::UI::OS.current.supports_arrow_keys?
209
+ 'Choose with ↑ ↓ ⏎'
210
+ else
211
+ "Navigate up with 'k' and down with 'j', press Enter to select"
212
+ end
213
+
214
+ instructions = (multiple ? 'Toggle options. ' : '') + navigate_text
215
+ instructions += ", filter with 'f'" if filter_ui
216
+ instructions += ", enter option with 'e'" if select_ui && (options.size > 9)
217
+ puts_question("#{question} {{yellow:(#{instructions})}}")
218
+ resp = interactive_prompt(options, multiple: multiple, default: default)
219
+
220
+ # Clear the line
221
+ print(ANSI.previous_line + ANSI.clear_to_end_of_line)
222
+ # Force StdoutRouter to prefix
223
+ print(ANSI.previous_line + "\n")
224
+
225
+ # reset the question to include the answer
226
+ resp_text = resp
227
+ if multiple
228
+ resp_text = case resp.size
229
+ when 0
230
+ '<nothing>'
231
+ when 1..2
232
+ resp.join(' and ')
233
+ else
234
+ "#{resp.size} items"
235
+ end
236
+ end
237
+ puts_question("#{question} (You chose: {{italic:#{resp_text}}})")
238
+
239
+ return handler.call(resp) if block_given?
240
+ resp
241
+ end
242
+
243
+ # Useful for stubbing in tests
244
+ def interactive_prompt(options, multiple: false, default: nil)
245
+ InteractiveOptions.call(options, multiple: multiple, default: default)
246
+ end
247
+
248
+ def write_default_over_empty_input(default)
249
+ CLI::UI.raw do
250
+ STDERR.puts(
251
+ CLI::UI::ANSI.cursor_up(1) +
252
+ "\r" +
253
+ CLI::UI::ANSI.cursor_forward(4) + # TODO: width
254
+ default +
255
+ CLI::UI::Color::RESET.code
256
+ )
257
+ end
258
+ end
259
+
260
+ def puts_question(str)
261
+ CLI::UI.with_frame_color(:blue) do
262
+ STDOUT.puts(CLI::UI.fmt('{{?}} ' + str))
263
+ end
264
+ end
265
+
266
+ def readline(is_file: false)
267
+ if is_file
268
+ Readline.completion_proc = Readline::FILENAME_COMPLETION_PROC
269
+ Readline.completion_append_character = ''
270
+ else
271
+ Readline.completion_proc = proc { |*| nil }
272
+ Readline.completion_append_character = ' '
273
+ end
274
+
275
+ # because Readline is a C library, CLI::UI's hooks into $stdout don't
276
+ # work. We could work around this by having CLI::UI use a pipe and a
277
+ # thread to manage output, but the current strategy feels like a
278
+ # better tradeoff.
279
+ prefix = CLI::UI.with_frame_color(:blue) { CLI::UI::Frame.prefix }
280
+ # If a prompt is interrupted on Windows it locks the colour of the terminal from that point on, so we should
281
+ # not change the colour here.
282
+ prompt = prefix + CLI::UI.fmt('{{blue:> }}')
283
+ prompt += CLI::UI::Color::YELLOW.code if CLI::UI::OS.current.supports_color_prompt?
284
+
285
+ begin
286
+ line = Readline.readline(prompt, true)
287
+ print(CLI::UI::Color::RESET.code)
288
+ line.to_s.chomp
289
+ rescue Interrupt
290
+ CLI::UI.raw { STDERR.puts('^C' + CLI::UI::Color::RESET.code) }
291
+ raise
292
+ end
293
+ end
294
+ end
295
+ end
296
+ end
297
+ end