highline 1.7.10 → 2.0.0.pre.develop.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.simplecov +5 -0
  4. data/.travis.yml +11 -6
  5. data/Changelog.md +112 -20
  6. data/Gemfile +8 -7
  7. data/README.rdoc +3 -0
  8. data/Rakefile +7 -2
  9. data/appveyor.yml +19 -0
  10. data/examples/asking_for_arrays.rb +3 -0
  11. data/examples/basic_usage.rb +3 -0
  12. data/examples/get_character.rb +3 -0
  13. data/examples/limit.rb +3 -0
  14. data/examples/menus.rb +3 -0
  15. data/examples/overwrite.rb +3 -0
  16. data/examples/password.rb +3 -0
  17. data/examples/repeat_entry.rb +4 -1
  18. data/lib/highline.rb +182 -704
  19. data/lib/highline/builtin_styles.rb +109 -0
  20. data/lib/highline/color_scheme.rb +4 -1
  21. data/lib/highline/compatibility.rb +2 -0
  22. data/lib/highline/custom_errors.rb +19 -0
  23. data/lib/highline/import.rb +4 -2
  24. data/lib/highline/list.rb +93 -0
  25. data/lib/highline/list_renderer.rb +232 -0
  26. data/lib/highline/menu.rb +20 -20
  27. data/lib/highline/paginator.rb +43 -0
  28. data/lib/highline/question.rb +157 -97
  29. data/lib/highline/question/answer_converter.rb +84 -0
  30. data/lib/highline/question_asker.rb +147 -0
  31. data/lib/highline/simulate.rb +5 -1
  32. data/lib/highline/statement.rb +58 -0
  33. data/lib/highline/string.rb +34 -0
  34. data/lib/highline/string_extensions.rb +3 -28
  35. data/lib/highline/style.rb +18 -8
  36. data/lib/highline/template_renderer.rb +38 -0
  37. data/lib/highline/terminal.rb +78 -0
  38. data/lib/highline/terminal/io_console.rb +98 -0
  39. data/lib/highline/terminal/ncurses.rb +38 -0
  40. data/lib/highline/terminal/unix_stty.rb +94 -0
  41. data/lib/highline/version.rb +3 -1
  42. data/lib/highline/wrapper.rb +43 -0
  43. data/test/acceptance/acceptance.rb +62 -0
  44. data/test/acceptance/acceptance_test.rb +69 -0
  45. data/test/acceptance/at_color_output_using_erb_templates.rb +17 -0
  46. data/test/acceptance/at_echo_false.rb +23 -0
  47. data/test/acceptance/at_readline.rb +37 -0
  48. data/test/io_console_compatible.rb +37 -0
  49. data/test/string_methods.rb +3 -0
  50. data/test/test_answer_converter.rb +26 -0
  51. data/test/{tc_color_scheme.rb → test_color_scheme.rb} +7 -9
  52. data/test/test_helper.rb +26 -0
  53. data/test/{tc_highline.rb → test_highline.rb} +193 -136
  54. data/test/{tc_import.rb → test_import.rb} +5 -2
  55. data/test/test_list.rb +60 -0
  56. data/test/{tc_menu.rb → test_menu.rb} +6 -3
  57. data/test/test_paginator.rb +73 -0
  58. data/test/test_question_asker.rb +20 -0
  59. data/test/test_simulator.rb +24 -0
  60. data/test/test_string_extension.rb +72 -0
  61. data/test/{tc_string_highline.rb → test_string_highline.rb} +7 -3
  62. data/test/{tc_style.rb → test_style.rb} +70 -35
  63. data/test/test_wrapper.rb +188 -0
  64. metadata +57 -22
  65. data/lib/highline/system_extensions.rb +0 -254
  66. data/test/tc_simulator.rb +0 -33
  67. data/test/tc_string_extension.rb +0 -33
@@ -1,3 +1,6 @@
1
+ # coding: utf-8
2
+
3
+ #--
1
4
  # menu.rb
2
5
  #
3
6
  # Created by Gregory Thomas Brown on 2005-05-10.
@@ -136,7 +139,7 @@ class HighLine
136
139
  def choice( name, help = nil, &action )
137
140
  @items << [name, action]
138
141
 
139
- @help[name.to_s.downcase] = help unless help.nil?
142
+ @help[name.to_s.downcase] = help if help
140
143
  update_responses # rebuild responses based on our settings
141
144
  end
142
145
 
@@ -154,7 +157,7 @@ class HighLine
154
157
  def hidden( name, help = nil, &action )
155
158
  @hidden_items << [name, action]
156
159
 
157
- @help[name.to_s.downcase] = help unless help.nil?
160
+ @help[name.to_s.downcase] = help if help
158
161
  end
159
162
 
160
163
  #
@@ -235,11 +238,10 @@ class HighLine
235
238
  # <tt>:menu_only</tt>:: Just the menu items, followed up by a likely
236
239
  # short _prompt_.
237
240
  # <i>any ERb String</i>:: Will be taken as the literal _layout_. This
238
- # String can access <tt>@header</tt>,
239
- # <tt>@menu</tt> and <tt>@prompt</tt>, but is
240
- # otherwise evaluated in the typical HighLine
241
- # context, to provide access to utilities like
242
- # HighLine.list() primarily.
241
+ # String can access <tt>header</tt>,
242
+ # <tt>menu</tt> and <tt>prompt</tt>, but is
243
+ # otherwise evaluated in the TemplateRenderer
244
+ # context so each method is properly delegated.
243
245
  #
244
246
  # If set to either <tt>:one_line</tt>, or <tt>:menu_only</tt>, _index_
245
247
  # will default to <tt>:none</tt> and _flow_ will default to
@@ -304,7 +306,7 @@ class HighLine
304
306
  end
305
307
 
306
308
  # Run or return it.
307
- if not action.nil?
309
+ if action
308
310
  @highline = highline_context
309
311
  if @shell
310
312
  result = action.call(name, details)
@@ -312,10 +314,8 @@ class HighLine
312
314
  result = action.call(name)
313
315
  end
314
316
  @nil_on_handled ? nil : result
315
- elsif action.nil?
316
- name
317
317
  else
318
- nil
318
+ name
319
319
  end
320
320
  ensure
321
321
  # make sure the hidden items are removed, before we return
@@ -348,19 +348,19 @@ class HighLine
348
348
  def to_s( )
349
349
  case @layout
350
350
  when :list
351
- '<%= if @header.nil? then '' else "#{@header}:\n" end %>' +
352
- "<%= list( @menu, #{@flow.inspect},
351
+ %(<%= header ? "#{header}:\n" : '' %>) +
352
+ "<%= list( menu, #{@flow.inspect},
353
353
  #{@list_option.inspect} ) %>" +
354
- "<%= @prompt %>"
354
+ "<%= prompt %>"
355
355
  when :one_line
356
- '<%= if @header.nil? then '' else "#{@header}: " end %>' +
357
- "<%= @prompt %>" +
358
- "(<%= list( @menu, #{@flow.inspect},
356
+ %(<%= header ? "#{header}: " : '' %>) +
357
+ "<%= prompt %>" +
358
+ "(<%= list( menu, #{@flow.inspect},
359
359
  #{@list_option.inspect} ) %>)" +
360
- "<%= @prompt[/\s*$/] %>"
360
+ "<%= prompt[/\s*$/] %>"
361
361
  when :menu_only
362
- "<%= list( @menu, #{@flow.inspect},
363
- #{@list_option.inspect} ) %><%= @prompt %>"
362
+ "<%= list( menu, #{@flow.inspect},
363
+ #{@list_option.inspect} ) %><%= prompt %>"
364
364
  else
365
365
  @layout
366
366
  end
@@ -0,0 +1,43 @@
1
+ # coding: utf-8
2
+
3
+ class HighLine
4
+ class Paginator
5
+ attr_reader :highline
6
+
7
+ def initialize(highline)
8
+ @highline = highline
9
+ end
10
+
11
+ #
12
+ # Page print a series of at most _page_at_ lines for _output_. After each
13
+ # page is printed, HighLine will pause until the user presses enter/return
14
+ # then display the next page of data.
15
+ #
16
+ # Note that the final page of _output_ is *not* printed, but returned
17
+ # instead. This is to support any special handling for the final sequence.
18
+ #
19
+ def page_print(text)
20
+ return text unless highline.page_at
21
+
22
+ lines = text.lines.to_a
23
+ while lines.size > highline.page_at
24
+ highline.puts lines.slice!(0...highline.page_at).join
25
+ highline.puts
26
+ # Return last line if user wants to abort paging
27
+ return "...\n#{lines.last}" unless continue_paging?
28
+ end
29
+ return lines.join
30
+ end
31
+
32
+ #
33
+ # Ask user if they wish to continue paging output. Allows them to type "q" to
34
+ # cancel the paging process.
35
+ #
36
+ def continue_paging?
37
+ command = highline.new_scope.ask(
38
+ "-- press enter/return to continue or q to stop -- "
39
+ ) { |q| q.character = true }
40
+ command !~ /\A[qQ]\Z/ # Only continue paging if Q was not hit.
41
+ end
42
+ end
43
+ end
@@ -1,3 +1,6 @@
1
+ # coding: utf-8
2
+
3
+ #--
1
4
  # question.rb
2
5
  #
3
6
  # Created by James Edward Gray II on 2005-04-26.
@@ -8,6 +11,7 @@
8
11
  require "optparse"
9
12
  require "date"
10
13
  require "pathname"
14
+ require "highline/question/answer_converter"
11
15
 
12
16
  class HighLine
13
17
  #
@@ -17,23 +21,32 @@ class HighLine
17
21
  # process is handled according to the users wishes.
18
22
  #
19
23
  class Question
20
- # An internal HighLine error. User code does not need to trap this.
21
- class NoAutoCompleteMatch < StandardError
22
- # do nothing, just creating a unique error type
24
+ include CustomErrors
25
+
26
+ #
27
+ # If _template_or_question_ is already a Question object just return it.
28
+ # If not, build it.
29
+ #
30
+ def self.build(template_or_question, answer_type = nil, &details)
31
+ if template_or_question.is_a? Question
32
+ template_or_question
33
+ else
34
+ Question.new(template_or_question, answer_type, &details)
35
+ end
23
36
  end
24
37
 
25
38
  #
26
- # Create an instance of HighLine::Question. Expects a _question_ to ask
39
+ # Create an instance of HighLine::Question. Expects a _template_ to ask
27
40
  # (can be <tt>""</tt>) and an _answer_type_ to convert the answer to.
28
41
  # The _answer_type_ parameter must be a type recognized by
29
42
  # Question.convert(). If given, a block is yielded the new Question
30
43
  # object to allow custom initialization.
31
44
  #
32
- def initialize( question, answer_type )
45
+ def initialize(template, answer_type)
33
46
  # initialize instance data
34
- @question = String(question).dup
47
+ @template = template.dup
35
48
  @answer_type = answer_type
36
- @completion = @answer_type
49
+ @completion = @answer_type
37
50
 
38
51
  @character = nil
39
52
  @limit = nil
@@ -63,7 +76,11 @@ class HighLine
63
76
  end
64
77
 
65
78
  # The ERb template of the question to be asked.
66
- attr_accessor :question
79
+ attr_accessor :template
80
+
81
+ # The answer, set by HighLine#ask
82
+ attr_accessor :answer
83
+
67
84
  # The type that will be used to convert this answer.
68
85
  attr_accessor :answer_type
69
86
  # For Auto-completion
@@ -214,12 +231,9 @@ class HighLine
214
231
  # Returns the provided _answer_string_ or the default answer for this
215
232
  # Question if a default was set and the answer is empty.
216
233
  #
217
- def answer_or_default( answer_string )
218
- if answer_string.length == 0 and not @default.nil?
219
- @default
220
- else
221
- answer_string
222
- end
234
+ def answer_or_default(answer_string)
235
+ return @default if answer_string.empty? && @default
236
+ answer_string
223
237
  end
224
238
 
225
239
  #
@@ -230,35 +244,28 @@ class HighLine
230
244
  def build_responses(message_source = answer_type, new_hash_wins = false)
231
245
  append_default if [::String, Symbol].include? default.class
232
246
 
233
- choice_error_str_func = lambda do
234
- message_source.is_a?(Array) \
235
- ? '[' + message_source.map { |s| "#{s}" }.join(', ') + ']' \
236
- : message_source.inspect
237
- end
238
-
239
247
  old_hash = @responses
240
248
 
241
- new_hash = { :ambiguous_completion =>
242
- "Ambiguous choice. Please choose one of " +
243
- choice_error_str_func.call + '.',
244
- :ask_on_error =>
245
- "? ",
246
- :invalid_type =>
247
- "You must enter a valid #{message_source}.",
248
- :no_completion =>
249
- "You must choose one of " + choice_error_str_func.call + '.',
250
- :not_in_range =>
251
- "Your answer isn't within the expected range " +
252
- "(#{expected_range}).",
253
- :mismatch =>
254
- "Your entries didn't match.",
255
- :not_valid =>
256
- "Your answer isn't valid (must match " +
257
- "#{@validate.inspect})." }
249
+ new_hash = build_responses_new_hash(message_source)
258
250
 
259
251
  @responses = new_hash_wins ? old_hash.merge(new_hash) : new_hash.merge(old_hash)
260
252
  end
261
253
 
254
+ def build_responses_new_hash(message_source)
255
+ { :ambiguous_completion => "Ambiguous choice. Please choose one of " +
256
+ choice_error_str(message_source) + '.',
257
+ :ask_on_error => "? ",
258
+ :invalid_type => "You must enter a valid #{message_source}.",
259
+ :no_completion => "You must choose one of " +
260
+ choice_error_str(message_source) + '.',
261
+ :not_in_range => "Your answer isn't within the expected range " +
262
+ "(#{expected_range}).",
263
+ :mismatch => "Your entries didn't match.",
264
+ :not_valid => "Your answer isn't valid (must match " +
265
+ "#{@validate.inspect})." }
266
+ end
267
+
268
+
262
269
  #
263
270
  # Returns the provided _answer_string_ after changing character case by
264
271
  # the rules of this Question. Valid settings for whitespace are:
@@ -273,7 +280,7 @@ class HighLine
273
280
  #
274
281
  # An unrecognized choice (like <tt>:none</tt>) is treated as +nil+.
275
282
  #
276
- def change_case( answer_string )
283
+ def change_case(answer_string)
277
284
  if [:up, :upcase].include?(@case)
278
285
  answer_string.upcase
279
286
  elsif [:down, :downcase].include?(@case)
@@ -314,46 +321,30 @@ class HighLine
314
321
  # This method throws ArgumentError, if the conversion cannot be
315
322
  # completed for any reason.
316
323
  #
317
- def convert( answer_string )
318
- if @answer_type.nil?
319
- answer_string
320
- elsif [::String, HighLine::String].include?(@answer_type)
321
- HighLine::String(answer_string)
322
- elsif [Float, Integer, String].include?(@answer_type)
323
- Kernel.send(@answer_type.to_s.to_sym, answer_string)
324
- elsif @answer_type == Symbol
325
- answer_string.to_sym
326
- elsif @answer_type == Regexp
327
- Regexp.new(answer_string)
328
- elsif @answer_type.is_a?(Array) or [File, Pathname].include?(@answer_type)
329
- # cheating, using OptionParser's Completion module
330
- choices = selection
331
- choices.extend(OptionParser::Completion)
332
- answer = choices.complete(answer_string)
333
- if answer.nil?
334
- raise NoAutoCompleteMatch
335
- end
336
- if @answer_type.is_a?(Array)
337
- answer.last
338
- elsif @answer_type == File
339
- File.open(File.join(@directory.to_s, answer.last))
340
- else
341
- Pathname.new(File.join(@directory.to_s, answer.last))
342
- end
343
- elsif [Date, DateTime].include?(@answer_type) or @answer_type.is_a?(Class)
344
- @answer_type.parse(answer_string)
345
- elsif @answer_type.is_a?(Proc)
346
- @answer_type[answer_string]
347
- end
324
+ def convert
325
+ AnswerConverter.new(self).convert
326
+ end
327
+
328
+ def check_range
329
+ raise NotInRangeQuestionError unless in_range?
330
+ end
331
+
332
+ def choices_complete(answer_string)
333
+ # cheating, using OptionParser's Completion module
334
+ choices = selection
335
+ choices.extend(OptionParser::Completion)
336
+ answer = choices.complete(answer_string)
337
+ raise NoAutoCompleteMatch unless answer
338
+ answer
348
339
  end
349
340
 
350
341
  # Returns an English explanation of the current range settings.
351
- def expected_range( )
342
+ def expected_range
352
343
  expected = [ ]
353
344
 
354
- expected << "above #{@above}" unless @above.nil?
355
- expected << "below #{@below}" unless @below.nil?
356
- expected << "included in #{@in.inspect}" unless @in.nil?
345
+ expected << "above #{@above}" if @above
346
+ expected << "below #{@below}" if @below
347
+ expected << "included in #{@in.inspect}" if @in
357
348
 
358
349
  case expected.size
359
350
  when 0 then ""
@@ -364,15 +355,15 @@ class HighLine
364
355
  end
365
356
 
366
357
  # Returns _first_answer_, which will be unset following this call.
367
- def first_answer( )
358
+ def first_answer
368
359
  @first_answer
369
360
  ensure
370
361
  @first_answer = nil
371
362
  end
372
363
 
373
364
  # Returns true if _first_answer_ is set.
374
- def first_answer?( )
375
- not @first_answer.nil?
365
+ def first_answer?
366
+ !!@first_answer
376
367
  end
377
368
 
378
369
  #
@@ -381,10 +372,10 @@ class HighLine
381
372
  # _in_ attribute. Otherwise, +false+ is returned. Any +nil+ attributes
382
373
  # are not checked.
383
374
  #
384
- def in_range?( answer_object )
385
- (@above.nil? or answer_object > @above) and
386
- (@below.nil? or answer_object < @below) and
387
- (@in.nil? or @in.include?(answer_object))
375
+ def in_range?
376
+ (!@above or answer > @above) and
377
+ (!@below or answer < @below) and
378
+ (!@in or @in.include?(answer))
388
379
  end
389
380
 
390
381
  #
@@ -406,8 +397,8 @@ class HighLine
406
397
  #
407
398
  # This process is skipped for single character input.
408
399
  #
409
- def remove_whitespace( answer_string )
410
- if @whitespace.nil?
400
+ def remove_whitespace(answer_string)
401
+ if !@whitespace
411
402
  answer_string
412
403
  elsif [:strip, :chomp].include?(@whitespace)
413
404
  answer_string.send(@whitespace)
@@ -423,12 +414,18 @@ class HighLine
423
414
  end
424
415
  end
425
416
 
417
+ def format_answer(answer_string)
418
+ answer_string = String(answer_string)
419
+ answer_string = remove_whitespace(answer_string)
420
+ change_case(answer_string)
421
+ end
422
+
426
423
  #
427
424
  # Returns an Array of valid answers to this question. These answers are
428
425
  # only known when _answer_type_ is set to an Array of choices, File, or
429
426
  # Pathname. Any other time, this method will return an empty Array.
430
427
  #
431
- def selection( )
428
+ def selection
432
429
  if @completion.is_a?(Array)
433
430
  @completion
434
431
  elsif [File, Pathname].include?(@completion)
@@ -440,9 +437,9 @@ class HighLine
440
437
  end
441
438
  end
442
439
 
443
- # Stringifies the question to be asked.
440
+ # Stringifies the template to be asked.
444
441
  def to_s
445
- @question
442
+ @template
446
443
  end
447
444
 
448
445
  #
@@ -452,10 +449,65 @@ class HighLine
452
449
  # It's important to realize that an answer is validated after whitespace
453
450
  # and case handling.
454
451
  #
455
- def valid_answer?( answer_string )
456
- @validate.nil? or
457
- (@validate.is_a?(Regexp) and answer_string =~ @validate) or
458
- (@validate.is_a?(Proc) and @validate[answer_string])
452
+ def valid_answer?
453
+ !@validate or
454
+ (@validate.is_a?(Regexp) and answer =~ @validate) or
455
+ (@validate.is_a?(Proc) and @validate[answer])
456
+ end
457
+
458
+ #
459
+ # Return a line or character of input, as requested for this question.
460
+ # Character input will be returned as a single character String,
461
+ # not an Integer.
462
+ #
463
+ # This question's _first_answer_ will be returned instead of input, if set.
464
+ #
465
+ # Raises EOFError if input is exhausted.
466
+ #
467
+ def get_response(highline)
468
+ return first_answer if first_answer?
469
+
470
+ case character
471
+ when :getc
472
+ highline.get_response_getc_mode(self)
473
+ when true
474
+ highline.get_response_character_mode(self)
475
+ else
476
+ highline.get_response_line_mode(self)
477
+ end
478
+ end
479
+
480
+ def get_response_or_default(highline)
481
+ self.answer = answer_or_default(get_response(highline))
482
+ end
483
+
484
+ def confirm_question(highline)
485
+ if confirm == true
486
+ "Are you sure? "
487
+ else
488
+ # evaluate ERb under initial scope, so it will have
489
+ # access to question and answer
490
+ template = ERB.new(confirm, nil, "%")
491
+ template_renderer = TemplateRenderer.new(template, self, highline)
492
+ template_renderer.render
493
+ end
494
+ end
495
+
496
+ def ask_on_error_msg
497
+ if responses[:ask_on_error] == :question
498
+ self
499
+ elsif responses[:ask_on_error]
500
+ responses[:ask_on_error]
501
+ end
502
+ end
503
+
504
+ # readline() needs to handle its own output, but readline only supports
505
+ # full line reading. Therefore if question.echo is anything but true,
506
+ # the prompt will not be issued. And we have to account for that now.
507
+ # Also, JRuby-1.7's ConsoleReader.readLine() needs to be passed the prompt
508
+ # to handle line editing properly.
509
+ def show_question(highline)
510
+ highline.say(self) unless (@readline && (@echo == true && !@limit))
459
511
  end
460
512
 
461
513
  private
@@ -465,15 +517,23 @@ class HighLine
465
517
  # Trailing whitespace is preserved so the function of HighLine.say() is
466
518
  # not affected.
467
519
  #
468
- def append_default( )
469
- if @question =~ /([\t ]+)\Z/
470
- @question << "|#{@default}|#{$1}"
471
- elsif @question == ""
472
- @question << "|#{@default}| "
473
- elsif @question[-1, 1] == "\n"
474
- @question[-2, 0] = " |#{@default}|"
520
+ def append_default
521
+ if @template =~ /([\t ]+)\Z/
522
+ @template << "|#{@default}|#{$1}"
523
+ elsif @template == ""
524
+ @template << "|#{@default}| "
525
+ elsif @template[-1, 1] == "\n"
526
+ @template[-2, 0] = " |#{@default}|"
527
+ else
528
+ @template << " |#{@default}|"
529
+ end
530
+ end
531
+
532
+ def choice_error_str(message_source)
533
+ if message_source.is_a? Array
534
+ '[' + message_source.join(', ') + ']'
475
535
  else
476
- @question << " |#{@default}|"
536
+ message_source.inspect
477
537
  end
478
538
  end
479
539
  end