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