1# frozen_string_literal: true 2#-- 3# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. 4# All rights reserved. 5# See LICENSE.txt for permissions. 6#++ 7 8require 'rubygems/util' 9require 'rubygems/deprecate' 10require 'rubygems/text' 11 12## 13# Module that defines the default UserInteraction. Any class including this 14# module will have access to the +ui+ method that returns the default UI. 15 16module Gem::DefaultUserInteraction 17 18 include Gem::Text 19 20 ## 21 # The default UI is a class variable of the singleton class for this 22 # module. 23 24 @ui = nil 25 26 ## 27 # Return the default UI. 28 29 def self.ui 30 @ui ||= Gem::ConsoleUI.new 31 end 32 33 ## 34 # Set the default UI. If the default UI is never explicitly set, a simple 35 # console based UserInteraction will be used automatically. 36 37 def self.ui=(new_ui) 38 @ui = new_ui 39 end 40 41 ## 42 # Use +new_ui+ for the duration of +block+. 43 44 def self.use_ui(new_ui) 45 old_ui = @ui 46 @ui = new_ui 47 yield 48 ensure 49 @ui = old_ui 50 end 51 52 ## 53 # See DefaultUserInteraction::ui 54 55 def ui 56 Gem::DefaultUserInteraction.ui 57 end 58 59 ## 60 # See DefaultUserInteraction::ui= 61 62 def ui=(new_ui) 63 Gem::DefaultUserInteraction.ui = new_ui 64 end 65 66 ## 67 # See DefaultUserInteraction::use_ui 68 69 def use_ui(new_ui, &block) 70 Gem::DefaultUserInteraction.use_ui(new_ui, &block) 71 end 72 73end 74 75## 76# UserInteraction allows RubyGems to interact with the user through standard 77# methods that can be replaced with more-specific UI methods for different 78# displays. 79# 80# Since UserInteraction dispatches to a concrete UI class you may need to 81# reference other classes for specific behavior such as Gem::ConsoleUI or 82# Gem::SilentUI. 83# 84# Example: 85# 86# class X 87# include Gem::UserInteraction 88# 89# def get_answer 90# n = ask("What is the meaning of life?") 91# end 92# end 93 94module Gem::UserInteraction 95 96 include Gem::DefaultUserInteraction 97 98 ## 99 # Displays an alert +statement+. Asks a +question+ if given. 100 101 def alert(statement, question = nil) 102 ui.alert statement, question 103 end 104 105 ## 106 # Displays an error +statement+ to the error output location. Asks a 107 # +question+ if given. 108 109 def alert_error(statement, question = nil) 110 ui.alert_error statement, question 111 end 112 113 ## 114 # Displays a warning +statement+ to the warning output location. Asks a 115 # +question+ if given. 116 117 def alert_warning(statement, question = nil) 118 ui.alert_warning statement, question 119 end 120 121 ## 122 # Asks a +question+ and returns the answer. 123 124 def ask(question) 125 ui.ask question 126 end 127 128 ## 129 # Asks for a password with a +prompt+ 130 131 def ask_for_password(prompt) 132 ui.ask_for_password prompt 133 end 134 135 ## 136 # Asks a yes or no +question+. Returns true for yes, false for no. 137 138 def ask_yes_no(question, default = nil) 139 ui.ask_yes_no question, default 140 end 141 142 ## 143 # Asks the user to answer +question+ with an answer from the given +list+. 144 145 def choose_from_list(question, list) 146 ui.choose_from_list question, list 147 end 148 149 ## 150 # Displays the given +statement+ on the standard output (or equivalent). 151 152 def say(statement = '') 153 ui.say statement 154 end 155 156 ## 157 # Terminates the RubyGems process with the given +exit_code+ 158 159 def terminate_interaction(exit_code = 0) 160 ui.terminate_interaction exit_code 161 end 162 163 ## 164 # Calls +say+ with +msg+ or the results of the block if really_verbose 165 # is true. 166 167 def verbose(msg = nil) 168 say(clean_text(msg || yield)) if Gem.configuration.really_verbose 169 end 170end 171 172## 173# Gem::StreamUI implements a simple stream based user interface. 174 175class Gem::StreamUI 176 177 extend Gem::Deprecate 178 179 ## 180 # The input stream 181 182 attr_reader :ins 183 184 ## 185 # The output stream 186 187 attr_reader :outs 188 189 ## 190 # The error stream 191 192 attr_reader :errs 193 194 ## 195 # Creates a new StreamUI wrapping +in_stream+ for user input, +out_stream+ 196 # for standard output, +err_stream+ for error output. If +usetty+ is true 197 # then special operations (like asking for passwords) will use the TTY 198 # commands to disable character echo. 199 200 def initialize(in_stream, out_stream, err_stream=STDERR, usetty=true) 201 @ins = in_stream 202 @outs = out_stream 203 @errs = err_stream 204 @usetty = usetty 205 end 206 207 ## 208 # Returns true if TTY methods should be used on this StreamUI. 209 210 def tty? 211 @usetty && @ins.tty? 212 end 213 214 ## 215 # Prints a formatted backtrace to the errors stream if backtraces are 216 # enabled. 217 218 def backtrace(exception) 219 return unless Gem.configuration.backtrace 220 221 @errs.puts "\t#{exception.backtrace.join "\n\t"}" 222 end 223 224 ## 225 # Choose from a list of options. +question+ is a prompt displayed above 226 # the list. +list+ is a list of option strings. Returns the pair 227 # [option_name, option_index]. 228 229 def choose_from_list(question, list) 230 @outs.puts question 231 232 list.each_with_index do |item, index| 233 @outs.puts " #{index+1}. #{item}" 234 end 235 236 @outs.print "> " 237 @outs.flush 238 239 result = @ins.gets 240 241 return nil, nil unless result 242 243 result = result.strip.to_i - 1 244 return list[result], result 245 end 246 247 ## 248 # Ask a question. Returns a true for yes, false for no. If not connected 249 # to a tty, raises an exception if default is nil, otherwise returns 250 # default. 251 252 def ask_yes_no(question, default=nil) 253 unless tty? 254 if default.nil? 255 raise Gem::OperationNotSupportedError, 256 "Not connected to a tty and no default specified" 257 else 258 return default 259 end 260 end 261 262 default_answer = case default 263 when nil 264 'yn' 265 when true 266 'Yn' 267 else 268 'yN' 269 end 270 271 result = nil 272 273 while result.nil? do 274 result = case ask "#{question} [#{default_answer}]" 275 when /^y/i then true 276 when /^n/i then false 277 when /^$/ then default 278 else nil 279 end 280 end 281 282 return result 283 end 284 285 ## 286 # Ask a question. Returns an answer if connected to a tty, nil otherwise. 287 288 def ask(question) 289 return nil if not tty? 290 291 @outs.print(question + " ") 292 @outs.flush 293 294 result = @ins.gets 295 result.chomp! if result 296 result 297 end 298 299 ## 300 # Ask for a password. Does not echo response to terminal. 301 302 def ask_for_password(question) 303 return nil if not tty? 304 305 @outs.print(question, " ") 306 @outs.flush 307 308 password = _gets_noecho 309 @outs.puts 310 password.chomp! if password 311 password 312 end 313 314 def require_io_console 315 @require_io_console ||= begin 316 begin 317 require 'io/console' 318 rescue LoadError 319 end 320 true 321 end 322 end 323 324 def _gets_noecho 325 require_io_console 326 @ins.noecho {@ins.gets} 327 end 328 329 ## 330 # Display a statement. 331 332 def say(statement="") 333 @outs.puts statement 334 end 335 336 ## 337 # Display an informational alert. Will ask +question+ if it is not nil. 338 339 def alert(statement, question=nil) 340 @outs.puts "INFO: #{statement}" 341 ask(question) if question 342 end 343 344 ## 345 # Display a warning on stderr. Will ask +question+ if it is not nil. 346 347 def alert_warning(statement, question=nil) 348 @errs.puts "WARNING: #{statement}" 349 ask(question) if question 350 end 351 352 ## 353 # Display an error message in a location expected to get error messages. 354 # Will ask +question+ if it is not nil. 355 356 def alert_error(statement, question=nil) 357 @errs.puts "ERROR: #{statement}" 358 ask(question) if question 359 end 360 361 ## 362 # Display a debug message on the same location as error messages. 363 364 def debug(statement) 365 @errs.puts statement 366 end 367 deprecate :debug, :none, 2018, 12 368 369 ## 370 # Terminate the application with exit code +status+, running any exit 371 # handlers that might have been defined. 372 373 def terminate_interaction(status = 0) 374 close 375 raise Gem::SystemExitException, status 376 end 377 378 def close 379 end 380 381 ## 382 # Return a progress reporter object chosen from the current verbosity. 383 384 def progress_reporter(*args) 385 case Gem.configuration.verbose 386 when nil, false 387 SilentProgressReporter.new(@outs, *args) 388 when true 389 SimpleProgressReporter.new(@outs, *args) 390 else 391 VerboseProgressReporter.new(@outs, *args) 392 end 393 end 394 395 ## 396 # An absolutely silent progress reporter. 397 398 class SilentProgressReporter 399 400 ## 401 # The count of items is never updated for the silent progress reporter. 402 403 attr_reader :count 404 405 ## 406 # Creates a silent progress reporter that ignores all input arguments. 407 408 def initialize(out_stream, size, initial_message, terminal_message = nil) 409 end 410 411 ## 412 # Does not print +message+ when updated as this object has taken a vow of 413 # silence. 414 415 def updated(message) 416 end 417 418 ## 419 # Does not print anything when complete as this object has taken a vow of 420 # silence. 421 422 def done 423 end 424 end 425 426 ## 427 # A basic dotted progress reporter. 428 429 class SimpleProgressReporter 430 431 include Gem::DefaultUserInteraction 432 433 ## 434 # The number of progress items counted so far. 435 436 attr_reader :count 437 438 ## 439 # Creates a new progress reporter that will write to +out_stream+ for 440 # +size+ items. Shows the given +initial_message+ when progress starts 441 # and the +terminal_message+ when it is complete. 442 443 def initialize(out_stream, size, initial_message, 444 terminal_message = "complete") 445 @out = out_stream 446 @total = size 447 @count = 0 448 @terminal_message = terminal_message 449 450 @out.puts initial_message 451 end 452 453 ## 454 # Prints out a dot and ignores +message+. 455 456 def updated(message) 457 @count += 1 458 @out.print "." 459 @out.flush 460 end 461 462 ## 463 # Prints out the terminal message. 464 465 def done 466 @out.puts "\n#{@terminal_message}" 467 end 468 469 end 470 471 ## 472 # A progress reporter that prints out messages about the current progress. 473 474 class VerboseProgressReporter 475 476 include Gem::DefaultUserInteraction 477 478 ## 479 # The number of progress items counted so far. 480 481 attr_reader :count 482 483 ## 484 # Creates a new progress reporter that will write to +out_stream+ for 485 # +size+ items. Shows the given +initial_message+ when progress starts 486 # and the +terminal_message+ when it is complete. 487 488 def initialize(out_stream, size, initial_message, 489 terminal_message = 'complete') 490 @out = out_stream 491 @total = size 492 @count = 0 493 @terminal_message = terminal_message 494 495 @out.puts initial_message 496 end 497 498 ## 499 # Prints out the position relative to the total and the +message+. 500 501 def updated(message) 502 @count += 1 503 @out.puts "#{@count}/#{@total}: #{message}" 504 end 505 506 ## 507 # Prints out the terminal message. 508 509 def done 510 @out.puts @terminal_message 511 end 512 end 513 514 ## 515 # Return a download reporter object chosen from the current verbosity 516 517 def download_reporter(*args) 518 if [nil, false].include?(Gem.configuration.verbose) || !@outs.tty? 519 SilentDownloadReporter.new(@outs, *args) 520 else 521 ThreadedDownloadReporter.new(@outs, *args) 522 end 523 end 524 525 ## 526 # An absolutely silent download reporter. 527 528 class SilentDownloadReporter 529 530 ## 531 # The silent download reporter ignores all arguments 532 533 def initialize(out_stream, *args) 534 end 535 536 ## 537 # The silent download reporter does not display +filename+ or care about 538 # +filesize+ because it is silent. 539 540 def fetch(filename, filesize) 541 end 542 543 ## 544 # Nothing can update the silent download reporter. 545 546 def update(current) 547 end 548 549 ## 550 # The silent download reporter won't tell you when the download is done. 551 # Because it is silent. 552 553 def done 554 end 555 end 556 557 ## 558 # A progress reporter that behaves nicely with threaded downloading. 559 560 class ThreadedDownloadReporter 561 562 MUTEX = Mutex.new 563 564 ## 565 # The current file name being displayed 566 567 attr_reader :file_name 568 569 ## 570 # Creates a new threaded download reporter that will display on 571 # +out_stream+. The other arguments are ignored. 572 573 def initialize(out_stream, *args) 574 @file_name = nil 575 @out = out_stream 576 end 577 578 ## 579 # Tells the download reporter that the +file_name+ is being fetched. 580 # The other arguments are ignored. 581 582 def fetch(file_name, *args) 583 if @file_name.nil? 584 @file_name = file_name 585 locked_puts "Fetching #{@file_name}" 586 end 587 end 588 589 ## 590 # Updates the threaded download reporter for the given number of +bytes+. 591 592 def update(bytes) 593 # Do nothing. 594 end 595 596 ## 597 # Indicates the download is complete. 598 599 def done 600 # Do nothing. 601 end 602 603 private 604 def locked_puts(message) 605 MUTEX.synchronize do 606 @out.puts message 607 end 608 end 609 end 610end 611 612## 613# Subclass of StreamUI that instantiates the user interaction using STDIN, 614# STDOUT, and STDERR. 615 616class Gem::ConsoleUI < Gem::StreamUI 617 618 ## 619 # The Console UI has no arguments as it defaults to reading input from 620 # stdin, output to stdout and warnings or errors to stderr. 621 622 def initialize 623 super STDIN, STDOUT, STDERR, true 624 end 625end 626 627## 628# SilentUI is a UI choice that is absolutely silent. 629 630class Gem::SilentUI < Gem::StreamUI 631 632 ## 633 # The SilentUI has no arguments as it does not use any stream. 634 635 def initialize 636 reader, writer = nil, nil 637 638 reader = File.open(IO::NULL, 'r') 639 writer = File.open(IO::NULL, 'w') 640 641 super reader, writer, writer, false 642 end 643 644 def close 645 super 646 @ins.close 647 @outs.close 648 end 649 650 def download_reporter(*args) # :nodoc: 651 SilentDownloadReporter.new(@outs, *args) 652 end 653 654 def progress_reporter(*args) # :nodoc: 655 SilentProgressReporter.new(@outs, *args) 656 end 657end 658