lib/cli/ui/prompt.rb in cli-ui-2.1.0 vs lib/cli/ui/prompt.rb in cli-ui-2.2.0
- old
+ new
@@ -27,10 +27,26 @@
autoload :OptionsHandler, 'cli/ui/prompt/options_handler'
class << self
extend T::Sig
+ sig { returns(Color) }
+ def instructions_color
+ @instructions_color ||= Color::YELLOW
+ end
+
+ # Set the instructions color.
+ #
+ # ==== Attributes
+ #
+ # * +color+ - the color to use for prompt instructions
+ #
+ sig { params(color: Colorable).void }
+ def instructions_color=(color)
+ @instructions_color = CLI::UI.resolve_color(color)
+ end
+
# Ask a user a question with either free form answer or a set of answers (multiple choice)
# Can use arrows, y/n, numbers (1/2), and vim bindings to control multiple choice selection
# Do not use this method for yes/no questions. Use +confirm+
#
# * Handles free form answers (options are nil)
@@ -165,23 +181,25 @@
# If the user simply presses "Enter" without typing any password, this will return an empty string.
sig { params(question: String).returns(String) }
def ask_password(question)
require 'io/console'
- STDOUT.print(CLI::UI.fmt('{{?}} ' + question)) # Do not use puts_question to avoid the new line.
+ CLI::UI::StdoutRouter::Capture.in_alternate_screen do
+ $stdout.print(CLI::UI.fmt('{{?}} ' + question)) # Do not use puts_question to avoid the new line.
- # noecho interacts poorly with Readline under system Ruby, so do a manual `gets` here.
- # No fancy Readline integration (like echoing back) is required for a password prompt anyway.
- password = STDIN.noecho do
- # Chomp will remove the one new line character added by `gets`, without touching potential extra spaces:
- # " 123 \n".chomp => " 123 "
- STDIN.gets.to_s.chomp
- end
+ # noecho interacts poorly with Readline under system Ruby, so do a manual `gets` here.
+ # No fancy Readline integration (like echoing back) is required for a password prompt anyway.
+ password = $stdin.noecho do
+ # Chomp will remove the one new line character added by `gets`, without touching potential extra spaces:
+ # " 123 \n".chomp => " 123 "
+ $stdin.gets.to_s.chomp
+ end
- STDOUT.puts # Complete the line
+ $stdout.puts # Complete the line
- password
+ password
+ end
end
# Asks the user a yes/no question.
# Can use arrows, y/n, numbers (1/2), and vim bindings to control
#
@@ -195,10 +213,40 @@
sig { params(question: String, default: T::Boolean).returns(T::Boolean) }
def confirm(question, default: true)
ask_interactive(question, default ? ['yes', 'no'] : ['no', 'yes'], filter_ui: false) == 'yes'
end
+ # Present the user with a message and wait for any key to be pressed, returning the pressed key.
+ #
+ # ==== Example Usage:
+ #
+ # CLI::UI::Prompt.any_key # Press any key to continue...
+ #
+ # CLI::UI::Prompt.any_key('Press RETURN to continue...') # Then check if that's what they pressed
+ sig { params(prompt: String).returns(T.nilable(String)) }
+ def any_key(prompt = 'Press any key to continue...')
+ CLI::UI::StdoutRouter::Capture.in_alternate_screen do
+ puts_question(prompt)
+ read_char
+ end
+ end
+
+ # Wait for any key to be pressed, returning the pressed key.
+ sig { returns(T.nilable(String)) }
+ def read_char
+ CLI::UI::StdoutRouter::Capture.in_alternate_screen do
+ if $stdin.tty? && !ENV['TEST']
+ require 'io/console'
+ $stdin.getch # raw mode for tty
+ else
+ $stdin.getc # returns nil at end of input
+ end
+ end
+ rescue Errno::EIO, Errno::EPIPE, IOError
+ "\e"
+ end
+
private
sig do
params(question: String, default: T.nilable(String), is_file: T::Boolean, allow_empty: T::Boolean)
.returns(String)
@@ -206,27 +254,29 @@
def ask_free_form(question, default, is_file, allow_empty)
if default && !allow_empty
raise(ArgumentError, 'conflicting arguments: default enabled but allow_empty is false')
end
- if default
- puts_question("#{question} (empty = #{default})")
- else
- puts_question(question)
- end
+ CLI::UI::StdoutRouter::Capture.in_alternate_screen do
+ if default
+ puts_question("#{question} (empty = #{default})")
+ else
+ puts_question(question)
+ end
- # Ask a free form question
- loop do
- line = readline(is_file: is_file)
+ # Ask a free form question
+ loop do
+ line = readline(is_file: is_file)
- if line.empty? && default
- write_default_over_empty_input(default)
- return default
- end
+ if line.empty? && default
+ write_default_over_empty_input(default)
+ return default
+ end
- if !line.empty? || allow_empty
- return line
+ if !line.empty? || allow_empty
+ return line
+ end
end
end
end
sig do
@@ -257,52 +307,59 @@
end
instructions = (multiple ? 'Toggle options. ' : '') + navigate_text
instructions += ", filter with 'f'" if filter_ui
instructions += ", enter option with 'e'" if select_ui && (options.size > 9)
- puts_question("#{question} {{yellow:(#{instructions})}}")
- resp = interactive_prompt(options, multiple: multiple, default: default)
- # Clear the line
- print(ANSI.previous_line + ANSI.clear_to_end_of_line)
- # Force StdoutRouter to prefix
- print(ANSI.previous_line + "\n")
+ CLI::UI::StdoutRouter::Capture.in_alternate_screen do
+ puts_question("#{question} " + instructions_color.code + "(#{instructions})" + Color::RESET.code)
+ resp = interactive_prompt(options, multiple: multiple, default: default)
- # reset the question to include the answer
- resp_text = case resp
- when Array
- case resp.size
- when 0
- '<nothing>'
- when 1..2
- resp.join(' and ')
+ # Clear the line
+ print(ANSI.previous_line + ANSI.clear_to_end_of_line)
+ # Force StdoutRouter to prefix
+ print(ANSI.previous_line + "\n")
+
+ # reset the question to include the answer
+ resp_text = case resp
+ when Array
+ case resp.size
+ when 0
+ '<nothing>'
+ when 1..2
+ resp.join(' and ')
+ else
+ "#{resp.size} items"
+ end
else
- "#{resp.size} items"
+ resp
end
- else
- resp
- end
- puts_question("#{question} (You chose: {{italic:#{resp_text}}})")
+ puts_question("#{question} (You chose: {{italic:#{resp_text}}})")
- return T.must(handler).call(resp) if block_given?
-
- resp
+ if block_given?
+ T.must(handler).call(resp)
+ else
+ resp
+ end
+ end
end
# Useful for stubbing in tests
sig do
params(options: T::Array[String], multiple: T::Boolean, default: T.nilable(T.any(T::Array[String], String)))
.returns(T.any(T::Array[String], String))
end
def interactive_prompt(options, multiple: false, default: nil)
- InteractiveOptions.call(options, multiple: multiple, default: default)
+ CLI::UI::StdoutRouter::Capture.in_alternate_screen do
+ InteractiveOptions.call(options, multiple: multiple, default: default)
+ end
end
sig { params(default: String).void }
def write_default_over_empty_input(default)
CLI::UI.raw do
- STDERR.puts(
+ $stderr.puts(
CLI::UI::ANSI.cursor_up(1) +
"\r" +
CLI::UI::ANSI.cursor_forward(4) + # TODO: width
default +
CLI::UI::Color::RESET.code,
@@ -310,11 +367,11 @@
end
end
sig { params(str: String).void }
def puts_question(str)
- STDOUT.puts(CLI::UI.fmt('{{?}} ' + str))
+ $stdout.puts(CLI::UI.fmt('{{?}} ' + str))
end
sig { params(is_file: T::Boolean).returns(String) }
def readline(is_file: false)
if is_file
@@ -338,10 +395,10 @@
begin
line = Readline.readline(prompt, true)
print(CLI::UI::Color::RESET.code)
line.to_s.chomp
rescue Interrupt
- CLI::UI.raw { STDERR.puts('^C' + CLI::UI::Color::RESET.code) }
+ CLI::UI.raw { $stderr.puts('^C' + CLI::UI::Color::RESET.code) }
raise
end
end
end
end