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