1#-- 2# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. 3# All rights reserved. 4# See LICENSE.txt for permissions. 5#++ 6 7## 8# Module that defines the default UserInteraction. Any class including this 9# module will have access to the +ui+ method that returns the default UI. 10 11module Gem::DefaultUserInteraction 12 13 ## 14 # The default UI is a class variable of the singleton class for this 15 # module. 16 17 @ui = nil 18 19 ## 20 # Return the default UI. 21 22 def self.ui 23 @ui ||= Gem::ConsoleUI.new 24 end 25 26 ## 27 # Set the default UI. If the default UI is never explicitly set, a simple 28 # console based UserInteraction will be used automatically. 29 30 def self.ui=(new_ui) 31 @ui = new_ui 32 end 33 34 ## 35 # Use +new_ui+ for the duration of +block+. 36 37 def self.use_ui(new_ui) 38 old_ui = @ui 39 @ui = new_ui 40 yield 41 ensure 42 @ui = old_ui 43 end 44 45 ## 46 # See DefaultUserInteraction::ui 47 48 def ui 49 Gem::DefaultUserInteraction.ui 50 end 51 52 ## 53 # See DefaultUserInteraction::ui= 54 55 def ui=(new_ui) 56 Gem::DefaultUserInteraction.ui = new_ui 57 end 58 59 ## 60 # See DefaultUserInteraction::use_ui 61 62 def use_ui(new_ui, &block) 63 Gem::DefaultUserInteraction.use_ui(new_ui, &block) 64 end 65 66end 67 68## 69# Make the default UI accessible without the "ui." prefix. Classes 70# including this module may use the interaction methods on the default UI 71# directly. Classes may also reference the ui and ui= methods. 72# 73# Example: 74# 75# class X 76# include Gem::UserInteraction 77# 78# def get_answer 79# n = ask("What is the meaning of life?") 80# end 81# end 82 83module Gem::UserInteraction 84 85 include Gem::DefaultUserInteraction 86 87 def alert(*args) 88 ui.alert(*args) 89 end 90 91 def alert_error(*args) 92 ui.alert_error(*args) 93 end 94 95 def alert_warning(*args) 96 ui.alert_warning(*args) 97 end 98 99 def ask(*args) 100 ui.ask(*args) 101 end 102 103 def ask_for_password(*args) 104 ui.ask_for_password(*args) 105 end 106 107 def ask_yes_no(*args) 108 ui.ask_yes_no(*args) 109 end 110 111 def choose_from_list(*args) 112 ui.choose_from_list(*args) 113 end 114 115 def say(*args) 116 ui.say(*args) 117 end 118 119 def terminate_interaction(*args) 120 ui.terminate_interaction(*args) 121 end 122end 123 124## 125# Gem::StreamUI implements a simple stream based user interface. 126 127class Gem::StreamUI 128 129 attr_reader :ins, :outs, :errs 130 131 def initialize(in_stream, out_stream, err_stream=STDERR, usetty=true) 132 @ins = in_stream 133 @outs = out_stream 134 @errs = err_stream 135 @usetty = usetty 136 end 137 138 def tty? 139 if RUBY_VERSION < '1.9.3' and RUBY_PLATFORM =~ /mingw|mswin/ then 140 @usetty 141 else 142 @usetty && @ins.tty? 143 end 144 end 145 146 ## 147 # Prints a formatted backtrace to the errors stream if backtraces are 148 # enabled. 149 150 def backtrace exception 151 return unless Gem.configuration.backtrace 152 153 @errs.puts "\t#{exception.backtrace.join "\n\t"}" 154 end 155 156 ## 157 # Choose from a list of options. +question+ is a prompt displayed above 158 # the list. +list+ is a list of option strings. Returns the pair 159 # [option_name, option_index]. 160 161 def choose_from_list(question, list) 162 @outs.puts question 163 164 list.each_with_index do |item, index| 165 @outs.puts " #{index+1}. #{item}" 166 end 167 168 @outs.print "> " 169 @outs.flush 170 171 result = @ins.gets 172 173 return nil, nil unless result 174 175 result = result.strip.to_i - 1 176 return list[result], result 177 end 178 179 ## 180 # Ask a question. Returns a true for yes, false for no. If not connected 181 # to a tty, raises an exception if default is nil, otherwise returns 182 # default. 183 184 def ask_yes_no(question, default=nil) 185 unless tty? then 186 if default.nil? then 187 raise Gem::OperationNotSupportedError, 188 "Not connected to a tty and no default specified" 189 else 190 return default 191 end 192 end 193 194 default_answer = case default 195 when nil 196 'yn' 197 when true 198 'Yn' 199 else 200 'yN' 201 end 202 203 result = nil 204 205 while result.nil? do 206 result = case ask "#{question} [#{default_answer}]" 207 when /^y/i then true 208 when /^n/i then false 209 when /^$/ then default 210 else nil 211 end 212 end 213 214 return result 215 end 216 217 ## 218 # Ask a question. Returns an answer if connected to a tty, nil otherwise. 219 220 def ask(question) 221 return nil if not tty? 222 223 @outs.print(question + " ") 224 @outs.flush 225 226 result = @ins.gets 227 result.chomp! if result 228 result 229 end 230 231 if RUBY_VERSION > '1.9.2' then 232 ## 233 # Ask for a password. Does not echo response to terminal. 234 235 def ask_for_password(question) 236 return nil if not tty? 237 238 require 'io/console' 239 240 @outs.print(question + " ") 241 @outs.flush 242 243 password = @ins.noecho {@ins.gets} 244 password.chomp! if password 245 password 246 end 247 else 248 ## 249 # Ask for a password. Does not echo response to terminal. 250 251 def ask_for_password(question) 252 return nil if not tty? 253 254 @outs.print(question + " ") 255 @outs.flush 256 257 Gem.win_platform? ? ask_for_password_on_windows : ask_for_password_on_unix 258 end 259 260 ## 261 # Asks for a password that works on windows. Ripped from the Heroku gem. 262 263 def ask_for_password_on_windows 264 return nil if not tty? 265 266 require "Win32API" 267 char = nil 268 password = '' 269 270 while char = Win32API.new("crtdll", "_getch", [ ], "L").Call do 271 break if char == 10 || char == 13 # received carriage return or newline 272 if char == 127 || char == 8 # backspace and delete 273 password.slice!(-1, 1) 274 else 275 password << char.chr 276 end 277 end 278 279 puts 280 password 281 end 282 283 ## 284 # Asks for a password that works on unix 285 286 def ask_for_password_on_unix 287 return nil if not tty? 288 289 system "stty -echo" 290 password = @ins.gets 291 password.chomp! if password 292 system "stty echo" 293 password 294 end 295 end 296 297 ## 298 # Display a statement. 299 300 def say(statement="") 301 @outs.puts statement 302 end 303 304 ## 305 # Display an informational alert. Will ask +question+ if it is not nil. 306 307 def alert(statement, question=nil) 308 @outs.puts "INFO: #{statement}" 309 ask(question) if question 310 end 311 312 ## 313 # Display a warning in a location expected to get error messages. Will 314 # ask +question+ if it is not nil. 315 316 def alert_warning(statement, question=nil) 317 @errs.puts "WARNING: #{statement}" 318 ask(question) if question 319 end 320 321 ## 322 # Display an error message in a location expected to get error messages. 323 # Will ask +question+ if it is not nil. 324 325 def alert_error(statement, question=nil) 326 @errs.puts "ERROR: #{statement}" 327 ask(question) if question 328 end 329 330 ## 331 # Display a debug message on the same location as error messages. 332 333 def debug(statement) 334 @errs.puts statement 335 end 336 337 ## 338 # Terminate the application with exit code +status+, running any exit 339 # handlers that might have been defined. 340 341 def terminate_interaction(status = 0) 342 raise Gem::SystemExitException, status 343 end 344 345 ## 346 # Return a progress reporter object chosen from the current verbosity. 347 348 def progress_reporter(*args) 349 if self.kind_of?(Gem::SilentUI) 350 return SilentProgressReporter.new(@outs, *args) 351 end 352 353 case Gem.configuration.verbose 354 when nil, false 355 SilentProgressReporter.new(@outs, *args) 356 when true 357 SimpleProgressReporter.new(@outs, *args) 358 else 359 VerboseProgressReporter.new(@outs, *args) 360 end 361 end 362 363 ## 364 # An absolutely silent progress reporter. 365 366 class SilentProgressReporter 367 attr_reader :count 368 369 def initialize(out_stream, size, initial_message, terminal_message = nil) 370 end 371 372 def updated(message) 373 end 374 375 def done 376 end 377 end 378 379 ## 380 # A basic dotted progress reporter. 381 382 class SimpleProgressReporter 383 384 include Gem::DefaultUserInteraction 385 386 attr_reader :count 387 388 def initialize(out_stream, size, initial_message, 389 terminal_message = "complete") 390 @out = out_stream 391 @total = size 392 @count = 0 393 @terminal_message = terminal_message 394 395 @out.puts initial_message 396 end 397 398 ## 399 # Prints out a dot and ignores +message+. 400 401 def updated(message) 402 @count += 1 403 @out.print "." 404 @out.flush 405 end 406 407 ## 408 # Prints out the terminal message. 409 410 def done 411 @out.puts "\n#{@terminal_message}" 412 end 413 414 end 415 416 ## 417 # A progress reporter that prints out messages about the current progress. 418 419 class VerboseProgressReporter 420 421 include Gem::DefaultUserInteraction 422 423 attr_reader :count 424 425 def initialize(out_stream, size, initial_message, 426 terminal_message = 'complete') 427 @out = out_stream 428 @total = size 429 @count = 0 430 @terminal_message = terminal_message 431 432 @out.puts initial_message 433 end 434 435 ## 436 # Prints out the position relative to the total and the +message+. 437 438 def updated(message) 439 @count += 1 440 @out.puts "#{@count}/#{@total}: #{message}" 441 end 442 443 ## 444 # Prints out the terminal message. 445 446 def done 447 @out.puts @terminal_message 448 end 449 end 450 451 ## 452 # Return a download reporter object chosen from the current verbosity 453 454 def download_reporter(*args) 455 if self.kind_of?(Gem::SilentUI) 456 return SilentDownloadReporter.new(@outs, *args) 457 end 458 459 case Gem.configuration.verbose 460 when nil, false 461 SilentDownloadReporter.new(@outs, *args) 462 else 463 VerboseDownloadReporter.new(@outs, *args) 464 end 465 end 466 467 ## 468 # An absolutely silent download reporter. 469 470 class SilentDownloadReporter 471 def initialize(out_stream, *args) 472 end 473 474 def fetch(filename, filesize) 475 end 476 477 def update(current) 478 end 479 480 def done 481 end 482 end 483 484 ## 485 # A progress reporter that prints out messages about the current progress. 486 487 class VerboseDownloadReporter 488 attr_reader :file_name, :total_bytes, :progress 489 490 def initialize(out_stream, *args) 491 @out = out_stream 492 @progress = 0 493 end 494 495 def fetch(file_name, total_bytes) 496 @file_name = file_name 497 @total_bytes = total_bytes.to_i 498 @units = @total_bytes.zero? ? 'B' : '%' 499 500 update_display(false) 501 end 502 503 def update(bytes) 504 new_progress = if @units == 'B' then 505 bytes 506 else 507 ((bytes.to_f * 100) / total_bytes.to_f).ceil 508 end 509 510 return if new_progress == @progress 511 512 @progress = new_progress 513 update_display 514 end 515 516 def done 517 @progress = 100 if @units == '%' 518 update_display(true, true) 519 end 520 521 private 522 523 def update_display(show_progress = true, new_line = false) 524 return unless @out.tty? 525 526 if show_progress then 527 @out.print "\rFetching: %s (%3d%s)" % [@file_name, @progress, @units] 528 else 529 @out.print "Fetching: %s" % @file_name 530 end 531 @out.puts if new_line 532 end 533 end 534end 535 536## 537# Subclass of StreamUI that instantiates the user interaction using STDIN, 538# STDOUT, and STDERR. 539 540class Gem::ConsoleUI < Gem::StreamUI 541 def initialize 542 super STDIN, STDOUT, STDERR, true 543 end 544end 545 546## 547# SilentUI is a UI choice that is absolutely silent. 548 549class Gem::SilentUI < Gem::StreamUI 550 def initialize 551 reader, writer = nil, nil 552 553 begin 554 reader = File.open('/dev/null', 'r') 555 writer = File.open('/dev/null', 'w') 556 rescue Errno::ENOENT 557 reader = File.open('nul', 'r') 558 writer = File.open('nul', 'w') 559 end 560 561 super reader, writer, writer, false 562 end 563 564 def download_reporter(*args) 565 SilentDownloadReporter.new(@outs, *args) 566 end 567 568 def progress_reporter(*args) 569 SilentProgressReporter.new(@outs, *args) 570 end 571end 572 573