tty-prompt 0.22.0 → 0.23.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ea89e6284f92e7de76bdd3457f6345912e11b7dc6c8c0249798aa0c88eb80759
4
- data.tar.gz: 4620f5a0473bf600646066a8e0f970993d310ef7a85ab0e5abd928b846c2765e
3
+ metadata.gz: 3dcdfb83f0945b162d19c5a068386009fbc5cc2f90969374018ee9aad72bd116
4
+ data.tar.gz: 004ebca9f017ea58bca8d4400e61d4817394b55343c0ecc3a3727f8818103d8f
5
5
  SHA512:
6
- metadata.gz: fd45c4a0690e88647f28762bdab648f1a71e85203691548e5e9a1a0a1775a02e670a512c52187f12720aadd17cdbba16f0fa4b302a5d1c9421c978224f9c6002
7
- data.tar.gz: 5d7c1b924060658c808ccb5358bed6d9b7deda6a4a6a13f3fb14382344ce502f68bc2c327d6234a59a6abf23c42c1da8c587b4da481dbd4403d66d1d955bf804
6
+ metadata.gz: e9fe7560e4dd4aca6e47d6ca67dca40079b4350ac97834d9bdca8c21254048097b338092177bebe5a1a2da1609fce1e155ee4a373c3d16c6979fc261a02dcc52
7
+ data.tar.gz: eff0e563cde0179831056cf107f61e17681269b705f7d499a657d84f365197500c5ea0220aad8b3959ef3d82bd0ac1db49d8beb4566881cb257112b2f69beafa
@@ -1,5 +1,16 @@
1
1
  # Change log
2
2
 
3
+ ## [v0.23.0] - 2020-12-14
4
+
5
+ ### Added
6
+ * Add the ability to provide an arbitrary array of values to Prompt::Slider by Katelyn Schiesser (@slowbro)
7
+
8
+ ### Changed
9
+ * Change to allow default option to be choice name as well as index in select, multi_select and enum_select prompts
10
+
11
+ ### Fixed
12
+ * Fix left and right key navigation while filtering choices in the #select and #multi_select prompts
13
+
3
14
  ## [v0.22.0] - 2020-07-20
4
15
 
5
16
  ### Added
@@ -43,7 +54,7 @@
43
54
  * Change gemspec to remove test artifacts
44
55
 
45
56
  ### Fixed
46
- * Fix :help_color option for multi_selct prompt by @robbystk
57
+ * Fix :help_color option for multi_select prompt by @robbystk
47
58
 
48
59
  ## [v0.20.0] - 2019-11-24
49
60
 
@@ -383,6 +394,7 @@
383
394
 
384
395
  * Initial implementation and release
385
396
 
397
+ [v0.23.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.22.0...v0.23.0
386
398
  [v0.22.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.21.0...v0.22.0
387
399
  [v0.21.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.20.0...v0.21.0
388
400
  [v0.20.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.19.0...v0.20.0
data/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  <div align="center">
2
- <a href="https://piotrmurach.github.io/tty" target="_blank"><img width="130" src="https://github.com/piotrmurach/tty/raw/master/images/tty.png" alt="tty logo" /></a>
2
+ <a href="https://ttytoolkit.org"><img width="130" src="https://github.com/piotrmurach/tty/raw/master/images/tty.png" alt="TTY Toolkit logo" /></a>
3
3
  </div>
4
4
 
5
5
  # TTY::Prompt [![Gitter](https://badges.gitter.im/Join%20Chat.svg)][gitter]
6
6
 
7
7
  [![Gem Version](https://badge.fury.io/rb/tty-prompt.svg)][gem]
8
- [![Build Status](https://secure.travis-ci.org/piotrmurach/tty-prompt.svg?branch=master)][travis]
8
+ [![Actions CI](https://github.com/piotrmurach/tty-prompt/workflows/CI/badge.svg?branch=master)][gh_actions_ci]
9
9
  [![Build status](https://ci.appveyor.com/api/projects/status/4cguoiah5dprbq7n?svg=true)][appveyor]
10
10
  [![Code Climate](https://codeclimate.com/github/piotrmurach/tty-prompt/badges/gpa.svg)][codeclimate]
11
11
  [![Coverage Status](https://coveralls.io/repos/github/piotrmurach/tty-prompt/badge.svg)][coverage]
@@ -13,6 +13,7 @@
13
13
 
14
14
  [gitter]: https://gitter.im/piotrmurach/tty
15
15
  [gem]: http://badge.fury.io/rb/tty-prompt
16
+ [gh_actions_ci]: https://github.com/piotrmurach/tty-prompt/actions?query=workflow%3ACI
16
17
  [travis]: http://travis-ci.org/piotrmurach/tty-prompt
17
18
  [appveyor]: https://ci.appveyor.com/project/piotrmurach/tty-prompt
18
19
  [codeclimate]: https://codeclimate.com/github/piotrmurach/tty-prompt
@@ -23,10 +24,6 @@
23
24
 
24
25
  **TTY::Prompt** provides independent prompt component for [TTY](https://github.com/piotrmurach/tty) toolkit.
25
26
 
26
- <div align="center">
27
- <img alt="Available prompts demo" src="assets/demo.gif" width="562" height="226">
28
- </div>
29
-
30
27
  ## Features
31
28
 
32
29
  * Number of prompt types for gathering user input
@@ -118,7 +115,7 @@ Or install it yourself as:
118
115
  * [3. settings](#3-settings)
119
116
  * [3.1 :symbols](#31-symbols)
120
117
  * [3.2 :active_color](#32-active_color)
121
- * [3.3 :enable_color](#33-enable-color)
118
+ * [3.3 :enable_color](#33-enable_color)
122
119
  * [3.4 :help_color](#34-help_color)
123
120
  * [3.5 :interrupt](#35-interrupt)
124
121
  * [3.6 :prefix](#36-prefix)
@@ -304,7 +301,7 @@ prompt.ask("Provide keys and values:", convert: :int_map)
304
301
  If a user provides a wrong type for conversion an error message will be printed in the console:
305
302
 
306
303
  ```ruby
307
- prompt.ask("Provide digit:", convert: float)
304
+ prompt.ask("Provide digit:", convert: :float)
308
305
  # Provide digit: x
309
306
  # >> Cannot convert `x` into 'float' type
310
307
  ```
@@ -312,8 +309,8 @@ prompt.ask("Provide digit:", convert: float)
312
309
  You can further customize error message:
313
310
 
314
311
  ```ruby
315
- prompt.ask("Provide digit:", convert: float) do |q|
316
- q.convert(:float, "Wrong value of %{value} for %{type} conversion"
312
+ prompt.ask("Provide digit:", convert: :float) do |q|
313
+ q.convert(:float, "Wrong value of %{value} for %{type} conversion")
317
314
  # or
318
315
  q.convert :float
319
316
  q.messages[:convert?] = "Wrong value of %{value} for %{type} conversion"
@@ -384,7 +381,7 @@ To change default range validation error message do:
384
381
  ```ruby
385
382
  prompt.ask("How spicy on scale (1-5)? ") do |q|
386
383
  q.in "1-5"
387
- q.messages[:range?] = "%{value} out of expected range #{in}"
384
+ q.messages[:range?] = "%{value} out of expected range %{in}"
388
385
  end
389
386
  ```
390
387
 
@@ -393,7 +390,7 @@ end
393
390
  In order to check that provided input falls inside a range of inputs use the `in` option. For example, if we wanted to ask a user for a single digit in given range we may do following:
394
391
 
395
392
  ```ruby
396
- ask("Provide number in range: 0-9?") { |q| q.in("0-9") }
393
+ prompt.ask("Provide number in range: 0-9?") { |q| q.in("0-9") }
397
394
  ```
398
395
 
399
396
  #### 2.1.7 `:modify`
@@ -439,13 +436,13 @@ In order to validate that input matches a given pattern you can pass the `valida
439
436
 
440
437
  ```ruby
441
438
  prompt.ask("What is your username?") do |q|
442
- q.validate /^[^\.]+\.[^\.]+/
439
+ q.validate(/\A[^.]+\.[^.]+\Z/)
443
440
  end
444
441
  ```
445
442
 
446
443
  ```ruby
447
444
  prompt.ask("What is your username?") do |q|
448
- q.validate { |input| input =~ /^[^\.]+\.[^\.]+/ }
445
+ q.validate ->(input) { input =~ /\A[^.]+\.[^.]+\Z/ }
449
446
  end
450
447
  ```
451
448
 
@@ -495,7 +492,8 @@ prompt.multiline("Description?")
495
492
  # I know not all that may be coming,
496
493
  # but be it what it will,
497
494
  # I'll go to it laughing.
498
- # => ["I know not all that may be coming,\n", "but be it what it will,\n", "I'll go to it laughing.\n"]
495
+ # =>
496
+ # ["I know not all that may be coming,\n", "but be it what it will,\n", "I'll go to it laughing.\n"]
499
497
  ```
500
498
 
501
499
  The `multiline` uses similar options to those supported by `ask` prompt. For example, to provide default description:
@@ -522,11 +520,11 @@ prompt.mask("What is your secret?")
522
520
  # => What is your secret? ••••
523
521
  ```
524
522
 
525
- The masking character can be changed by passing `:symbols` option with `:mask` key:
523
+ The masking character can be changed by passing the `:mask` key:
526
524
 
527
525
  ```ruby
528
526
  heart = prompt.decorate(prompt.symbols[:heart] + " ", :magenta)
529
- prompt.mask("What is your secret?", symbols: {mask: heart})
527
+ prompt.mask("What is your secret?", mask: heart)
530
528
  # => What is your secret? ❤ ❤ ❤ ❤ ❤
531
529
  ```
532
530
 
@@ -615,6 +613,12 @@ By default the choice name is also the value the prompt will return when selecte
615
613
 
616
614
  ```ruby
617
615
  choices = {small: 1, medium: 2, large: 3}
616
+ prompt.select("What size?", choices)
617
+ # =>
618
+ # What size? (Press ↑/↓ arrow to move and Enter to select)
619
+ # ‣ small
620
+ # medium
621
+ # large
618
622
  ```
619
623
 
620
624
  Finally, you can define an array of choices where each choice is a hash value with `:name` & `:value` keys which can include other options for customising individual choices:
@@ -637,6 +641,11 @@ prompt.select("What size?") do |menu|
637
641
  menu.choice name: "medium", value: 2, disabled: "(out of stock)"
638
642
  menu.choice name: "large", value: 3
639
643
  end
644
+ # =>
645
+ # What size? (Press ↑/↓ arrow to move and Enter to select)
646
+ # ‣ small
647
+ # ✘ medium (out of stock)
648
+ # large
640
649
  ```
641
650
 
642
651
  or in a more compact way:
@@ -711,11 +720,12 @@ choices = {"Scorpion" => 1, "Kano" => 2, "Jax" => 3}
711
720
  prompt.select("Choose your destiny?", choices)
712
721
  ```
713
722
 
714
- To mark particular answer as selected use `default` with index of the option starting from `1`:
723
+ To mark particular answer as selected use `default` with either an index of the choice starting from `1` or a choice's name:
715
724
 
716
725
  ```ruby
717
726
  prompt.select("Choose your destiny?") do |menu|
718
727
  menu.default 3
728
+ # or menu.default "Jax"
719
729
 
720
730
  menu.choice "Scorpion", 1
721
731
  menu.choice "Kano", 2
@@ -728,7 +738,6 @@ end
728
738
  # ‣ Jax
729
739
  ```
730
740
 
731
-
732
741
  #### 2.6.2.1 `:cycle`
733
742
 
734
743
  You can navigate the choices using the arrow keys or define your own key mappings (see [keyboard events](#212-keyboard-events). When reaching the top/bottom of the list, the selection does not cycle around by default. If you wish to enable cycling, you can pass `cycle: true` to `select` and `multi_select`:
@@ -924,11 +933,12 @@ prompt.multi_select("Select drinks?") do |menu|
924
933
  end
925
934
  ```
926
935
 
927
- To mark choice(s) as selected use the `default` option with index(s) of the option(s) starting from `1`:
936
+ To mark choice(s) as selected use the `default` option with either index(s) of the choice(s) starting from `1` or choice name(s):
928
937
 
929
938
  ```ruby
930
939
  prompt.multi_select("Select drinks?") do |menu|
931
940
  menu.default 2, 5
941
+ # or menu.default :beer, :whisky
932
942
 
933
943
  menu.choice :vodka, {score: 10}
934
944
  menu.choice :beer, {score: 20}
@@ -1158,12 +1168,13 @@ end
1158
1168
  # Select an editor? /bin/nano
1159
1169
  ```
1160
1170
 
1161
- You can change the indexed numbers by passing `enum` option and the default option by using `default` like so
1171
+ You can change the indexed numbers formatting by passing `enum` option. The `default` option lets you specify which choice to mark as selected by default. It accepts an index of the choice starting from `1` or a choice name:
1162
1172
 
1163
1173
  ```ruby
1164
1174
  choices = %w(nano vim emacs)
1165
1175
  prompt.enum_select("Select an editor?") do |menu|
1166
1176
  menu.default 2
1177
+ # or menu.defualt "/usr/bin/vim"
1167
1178
  menu.enum "."
1168
1179
 
1169
1180
  menu.choice :nano, "/bin/nano"
@@ -1377,17 +1388,26 @@ prompt.suggest("b", possible, indent: 4, single_text: "Perhaps you meant?")
1377
1388
 
1378
1389
  ### 2.10 slider
1379
1390
 
1380
- If you have constrained range of numbers for user to choose from you may consider using `slider`.
1391
+ If you'd rather not display all possible values in a vertical list, you may consider using `slider`. The slider provides easy visual way of picking a value marked by `●` symbol.
1381
1392
 
1382
- The slider provides easy visual way of picking a value marked by `●` symbol. You can set `:min`(defaults to 0), `:max` and `:step`(defaults to 1) options to configure slider range:
1393
+ For integers, you can set `:min`(defaults to 0), `:max` and `:step`(defaults to 1) options to configure slider range:
1383
1394
 
1384
1395
  ```ruby
1385
- prompt.slider("Volume", max: 100, step: 5)
1396
+ prompt.slider("Volume", min: 0, max: 100, step: 5)
1386
1397
  # =>
1387
1398
  # Volume ──────────●────────── 50
1388
1399
  # (Use ←/→ arrow keys, press Enter to select)
1389
1400
  ```
1390
1401
 
1402
+ For everything else, you can provide an array of your desired choices:
1403
+
1404
+ ```ruby
1405
+ prompt.slider("Letter", ('a'..'z').to_a)
1406
+ # =>
1407
+ # Letter ────────────●───────────── m
1408
+ # (Use ←/→ arrow keys, press Enter to select)
1409
+ ```
1410
+
1391
1411
  By default the slider is configured to pick middle of the range as a start value, you can change this by using the `:default` option:
1392
1412
 
1393
1413
  ```ruby
@@ -1397,6 +1417,15 @@ prompt.slider("Volume", max: 100, step: 5, default: 75)
1397
1417
  # (Use ←/→ arrow keys, press Enter to select)
1398
1418
  ```
1399
1419
 
1420
+ You can also select the default value by name:
1421
+
1422
+ ```ruby
1423
+ prompt.slider("Letter", ('a'..'z').to_a, default: 'q')
1424
+ # =>
1425
+ # Letter ──────────────────●─────── q
1426
+ # (Use ←/→ arrow keys, press Enter to select)
1427
+ ```
1428
+
1400
1429
  You can also change the default slider formatting using the `:format`. The value must contain the `:slider` token to show current value and any `sprintf` compatible flag for number display, in our case `%d`:
1401
1430
 
1402
1431
  ```ruby
@@ -1450,13 +1479,25 @@ prompt.slider("What size?") do |range|
1450
1479
  range.max 100
1451
1480
  range.step 5
1452
1481
  range.default 75
1453
- range.format "|:slider| %d%"
1482
+ range.format "|:slider| %d%%"
1454
1483
  end
1455
1484
  # =>
1456
1485
  # Volume |───────────────●──────| 75%
1457
1486
  # (Use ←/→ arrow keys, press Enter to select)
1458
1487
  ```
1459
1488
 
1489
+ ```ruby
1490
+ prompt.slider("What letter?") do |range|
1491
+ range.choices ('a'..'z').to_a
1492
+ range.format "|:slider| %s"
1493
+ range.default 'q'
1494
+ end
1495
+ # =>
1496
+ # What letter? |──────────────────●───────| q
1497
+ # (Use ←/→ arrow keys, press Enter to select)
1498
+ ```
1499
+
1500
+
1460
1501
  ### 2.11 say
1461
1502
 
1462
1503
  To simply print message out to standard output use `say` like so:
@@ -1640,7 +1681,7 @@ prompt = TTY::Prompt.new(help_color: notice)
1640
1681
 
1641
1682
  Or use coloring of your own choice:
1642
1683
 
1643
- ```
1684
+ ```ruby
1644
1685
  prompt = TTY::Prompt.new(help_color: ->(str) { my-color-gem(str) })
1645
1686
  ```
1646
1687
 
@@ -1657,7 +1698,7 @@ Please [see pastel](https://github.com/piotrmurach/pastel#3-supported-colors) fo
1657
1698
  By default `InputInterrupt` error will be raised when the user hits the interrupt key(Control-C). However, you can customise this behaviour by passing the `:interrupt` option. The available options are:
1658
1699
 
1659
1700
  * `:signal` - sends interrupt signal
1660
- * `:exit` - exists with status code
1701
+ * `:exit` - exits with status code
1661
1702
  * `:noop` - skips handler
1662
1703
  * custom proc
1663
1704
 
@@ -388,16 +388,20 @@ module TTY
388
388
  # @example
389
389
  # prompt = TTY::Prompt.new
390
390
  # prompt.slider("What size?", min: 32, max: 54, step: 2)
391
+ # prompt.slider("What size?", [ 'xs', 's', 'm', 'l', 'xl' ])
391
392
  #
392
393
  # @param [String] question
393
394
  # the question to ask
394
395
  #
396
+ # @param [Array] choices
397
+ # the choices to display
398
+ #
395
399
  # @return [String]
396
400
  #
397
401
  # @api public
398
- def slider(question, **options, &block)
402
+ def slider(question, choices = nil, **options, &block)
399
403
  slider = Slider.new(self, **options)
400
- slider.call(question, &block)
404
+ slider.call(question, choices, &block)
401
405
  end
402
406
 
403
407
  # Print statement out. If the supplied message ends with a space or
@@ -111,9 +111,10 @@ module TTY
111
111
  # @api public
112
112
  def ==(other)
113
113
  return false unless other.is_a?(self.class)
114
+
114
115
  name == other.name &&
115
- value == other.value &&
116
- key == other.key
116
+ value == other.value &&
117
+ key == other.key
117
118
  end
118
119
 
119
120
  # Object string representation
@@ -122,7 +123,7 @@ module TTY
122
123
  #
123
124
  # @api public
124
125
  def to_s
125
- "#{name}"
126
+ name.to_s
126
127
  end
127
128
  end # Choice
128
129
  end # Prompt
@@ -62,6 +62,7 @@ module TTY
62
62
  # @api public
63
63
  def each(&block)
64
64
  return to_enum unless block_given?
65
+
65
66
  choices.each(&block)
66
67
  end
67
68
 
@@ -100,7 +101,7 @@ module TTY
100
101
 
101
102
  # Find a matching choice
102
103
  #
103
- # @exmaple
104
+ # @example
104
105
  # choices.find_by(:name, "small")
105
106
  #
106
107
  # @param [Symbol] attr
@@ -38,6 +38,7 @@ module TTY
38
38
  # @api public
39
39
  def suffix(value = (not_set = true))
40
40
  return @negative if not_set
41
+
41
42
  @suffix = value
42
43
  end
43
44
 
@@ -46,6 +47,7 @@ module TTY
46
47
  # @api public
47
48
  def positive(value = (not_set = true))
48
49
  return @positive if not_set
50
+
49
51
  @positive = value
50
52
  end
51
53
 
@@ -54,11 +56,13 @@ module TTY
54
56
  # @api public
55
57
  def negative(value = (not_set = true))
56
58
  return @negative if not_set
59
+
57
60
  @negative = value
58
61
  end
59
62
 
60
63
  def call(message, &block)
61
64
  return if Utils.blank?(message)
65
+
62
66
  @message = message
63
67
  block.call(self) if block
64
68
  setup_defaults
@@ -15,6 +15,9 @@ module TTY
15
15
  class EnumList
16
16
  PAGE_HELP = "(Press tab/right or left to reveal more choices)"
17
17
 
18
+ # Checks type of default parameter to be integer
19
+ INTEGER_MATCHER = /\A[-+]?\d+\Z/.freeze
20
+
18
21
  # Create instance of EnumList menu.
19
22
  #
20
23
  # @api public
@@ -22,11 +25,11 @@ module TTY
22
25
  @prompt = prompt
23
26
  @prefix = options.fetch(:prefix) { @prompt.prefix }
24
27
  @enum = options.fetch(:enum) { ")" }
25
- @default = options.fetch(:default) { -1 }
28
+ @default = options.fetch(:default, nil)
26
29
  @active_color = options.fetch(:active_color) { @prompt.active_color }
27
30
  @help_color = options.fetch(:help_color) { @prompt.help_color }
28
31
  @error_color = options.fetch(:error_color) { @prompt.error_color }
29
- @cycle = options.fetch(:cycle) { false }
32
+ @cycle = options.fetch(:cycle, false)
30
33
  @quiet = options.fetch(:quiet) { @prompt.quiet }
31
34
  @symbols = @prompt.symbols.merge(options.fetch(:symbols, {}))
32
35
  @input = nil
@@ -49,6 +52,7 @@ module TTY
49
52
  # @api public
50
53
  def symbols(new_symbols = (not_set = true))
51
54
  return @symbols if not_set
55
+
52
56
  @symbols.merge!(new_symbols)
53
57
  end
54
58
 
@@ -65,7 +69,7 @@ module TTY
65
69
  #
66
70
  # @api public
67
71
  def default?
68
- @default > 0
72
+ !@default.to_s.empty?
69
73
  end
70
74
 
71
75
  # Set number of items per page
@@ -153,6 +157,7 @@ module TTY
153
157
  def keypress(event)
154
158
  if %i[backspace delete].include?(event.key.name)
155
159
  return if @input.empty?
160
+
156
161
  @input.chop!
157
162
  mark_choice_as_active
158
163
  elsif event.value =~ /^\d+$/
@@ -195,7 +200,6 @@ module TTY
195
200
 
196
201
  private
197
202
 
198
-
199
203
  # Find active choice or set to default
200
204
  #
201
205
  # @return [nil]
@@ -220,6 +224,8 @@ module TTY
220
224
  def validate_defaults
221
225
  msg = if @default.nil? || @default.to_s.empty?
222
226
  "default index must be an integer in range (1 - #{choices.size})"
227
+ elsif @default.to_s !~ INTEGER_MATCHER
228
+ validate_default_name
223
229
  elsif @default < 1 || @default > @choices.size
224
230
  "default index #{@default} out of range (1 - #{@choices.size})"
225
231
  elsif choices[@default - 1] && choices[@default - 1].disabled?
@@ -229,14 +235,31 @@ module TTY
229
235
  raise(ConfigurationError, msg) if msg
230
236
  end
231
237
 
238
+ # Validate default choice name
239
+ #
240
+ # @return [String]
241
+ #
242
+ # @api private
243
+ def validate_default_name
244
+ default_choice = choices.find_by(:name, @default.to_s)
245
+ if default_choice.nil?
246
+ "no choice found for the default name: #{@default.inspect}"
247
+ elsif default_choice.disabled?
248
+ "default name #{@default.inspect} matches disabled choice"
249
+ end
250
+ end
251
+
232
252
  # Setup default option and active selection
233
253
  #
234
254
  # @api private
235
255
  def setup_defaults
236
- if !default?
237
- @default = (0..choices.length).find {|i| !choices[i].disabled? } + 1
256
+ if @default.to_s.empty?
257
+ @default = (0..choices.length).find { |i| !choices[i].disabled? } + 1
238
258
  end
239
259
  validate_defaults
260
+ if default_choice = choices.find_by(:name, @default)
261
+ @default = choices.index(default_choice) + 1
262
+ end
240
263
  mark_choice_as_active
241
264
  end
242
265
 
@@ -342,6 +365,7 @@ module TTY
342
365
  def render_header
343
366
  return "" unless @done
344
367
  return "" unless @active
368
+
345
369
  selected_item = @choices[@active - 1].name.to_s
346
370
  @prompt.decorate(selected_item, @active_color)
347
371
  end
@@ -362,6 +386,7 @@ module TTY
362
386
  # @api private
363
387
  def page_help_message
364
388
  return "" unless paginated?
389
+
365
390
  "\n" + @prompt.decorate(@page_help, @help_color)
366
391
  end
367
392
 
@@ -23,7 +23,7 @@ module TTY
23
23
  def check(proc = nil, &block)
24
24
  results << (proc || block)
25
25
  end
26
- alias_method :<<, :check
26
+ alias << check
27
27
  end # Evaluator
28
28
  end # Prompt
29
29
  end # TTY
@@ -15,14 +15,17 @@ module TTY
15
15
  value: :help
16
16
  }.freeze
17
17
 
18
+ # Names for delete keys
19
+ DELETE_KEYS = %i[backspace delete].freeze
20
+
18
21
  # Create instance of Expander
19
22
  #
20
23
  # @api public
21
24
  def initialize(prompt, options = {})
22
25
  @prompt = prompt
23
26
  @prefix = options.fetch(:prefix) { @prompt.prefix }
24
- @default = options.fetch(:default) { 1 }
25
- @auto_hint = options.fetch(:auto_hint) { false }
27
+ @default = options.fetch(:default, 1)
28
+ @auto_hint = options.fetch(:auto_hint, false)
26
29
  @active_color = options.fetch(:active_color) { @prompt.active_color }
27
30
  @help_color = options.fetch(:help_color) { @prompt.help_color }
28
31
  @quiet = options.fetch(:quiet) { @prompt.quiet }
@@ -75,7 +78,7 @@ module TTY
75
78
  #
76
79
  # @api public
77
80
  def keypress(event)
78
- if [:backspace, :delete].include?(event.key.name)
81
+ if DELETE_KEYS.include?(event.key.name)
79
82
  @input.chop! unless @input.empty?
80
83
  elsif event.value =~ /^[^\e\n\r]/
81
84
  @input += event.value
@@ -101,6 +104,7 @@ module TTY
101
104
  # @api public
102
105
  def default(value = (not_set = true))
103
106
  return @default if not_set
107
+
104
108
  @default = value
105
109
  end
106
110
 
@@ -30,6 +30,7 @@ module TTY
30
30
 
31
31
  def countdown(value = (not_set = true))
32
32
  return @countdown if not_set
33
+
33
34
  @countdown = value
34
35
  end
35
36
 
@@ -81,6 +82,7 @@ module TTY
81
82
 
82
83
  @timer.while_remaining do |remaining|
83
84
  break if @done
85
+
84
86
  @input = @prompt.read_keypress(nonblock: true)
85
87
  end
86
88
  countdown(0) unless @done
@@ -16,6 +16,9 @@ module TTY
16
16
  # Allowed keys for filter, along with backspace and canc.
17
17
  FILTER_KEYS_MATCHER = /\A([[:alnum:]]|[[:punct:]])\Z/.freeze
18
18
 
19
+ # Checks type of default parameter to be integer
20
+ INTEGER_MATCHER = /\A\d+\Z/.freeze
21
+
19
22
  # Create instance of TTY::Prompt::List menu.
20
23
  #
21
24
  # @param Hash options
@@ -65,6 +68,7 @@ module TTY
65
68
  # @api public
66
69
  def symbols(new_symbols = (not_set = true))
67
70
  return @symbols if not_set
71
+
68
72
  @symbols.merge!(new_symbols)
69
73
  end
70
74
 
@@ -255,6 +259,7 @@ module TTY
255
259
  value = event.value.to_i
256
260
  return unless (1..choices.count).cover?(value)
257
261
  return if choices[value - 1].disabled?
262
+
258
263
  @active = value
259
264
  end
260
265
 
@@ -308,17 +313,19 @@ module TTY
308
313
  # When the choice on a page is outside of next page range then
309
314
  # adjust it to the last item, otherwise leave unchanged.
310
315
  def keyright(*)
311
- if (@active + page_size) <= @choices.size
312
- searchable = ((@active + page_size)..choices.length)
316
+ choices_size = choices.size
317
+ if (@active + page_size) <= choices_size
318
+ searchable = ((@active + page_size)..choices_size)
313
319
  @active = search_choice_in(searchable)
314
- elsif @active <= @choices.size # last page shorter
320
+ elsif @active <= choices_size # last page shorter
315
321
  current = @active % page_size
316
- remaining = @choices.size % page_size
322
+ remaining = choices_size % page_size
323
+
317
324
  if current.zero? || (remaining > 0 && current > remaining)
318
- searchable = @choices.size.downto(0).to_a
325
+ searchable = choices_size.downto(0).to_a
319
326
  @active = search_choice_in(searchable)
320
327
  elsif @cycle
321
- searchable = ((current.zero? ? page_size : current)..choices.length)
328
+ searchable = ((current.zero? ? page_size : current)..choices_size)
322
329
  @active = search_choice_in(searchable)
323
330
  end
324
331
  end
@@ -330,10 +337,10 @@ module TTY
330
337
 
331
338
  def keyleft(*)
332
339
  if (@active - page_size) > 0
333
- searchable = ((@active - page_size)..choices.length)
340
+ searchable = ((@active - page_size)..choices.size)
334
341
  @active = search_choice_in(searchable)
335
342
  elsif @cycle
336
- searchable = @choices.size.downto(1).to_a
343
+ searchable = choices.size.downto(1).to_a
337
344
  @active = search_choice_in(searchable)
338
345
  end
339
346
  @paging_changed = !@by_page
@@ -375,14 +382,19 @@ module TTY
375
382
 
376
383
  # Setup default option and active selection
377
384
  #
385
+ # @return [Integer]
386
+ #
378
387
  # @api private
379
388
  def setup_defaults
380
389
  validate_defaults
381
390
 
382
- if !@default.empty?
391
+ if @default.empty?
392
+ # no default, pick the first non-disabled choice
393
+ @active = choices.index { |choice| !choice.disabled? } + 1
394
+ elsif @default.first.to_s =~ INTEGER_MATCHER
383
395
  @active = @default.first
384
- else
385
- @active = @choices.index { |choice| !choice.disabled? } + 1
396
+ elsif default_choice = choices.find_by(:name, @default.first)
397
+ @active = choices.index(default_choice) + 1
386
398
  end
387
399
  end
388
400
 
@@ -397,16 +409,35 @@ module TTY
397
409
  @default.each do |d|
398
410
  msg = if d.nil? || d.to_s.empty?
399
411
  "default index must be an integer in range (1 - #{choices.size})"
412
+ elsif d.to_s !~ INTEGER_MATCHER
413
+ validate_default_name(d)
400
414
  elsif d < 1 || d > choices.size
401
415
  "default index `#{d}` out of range (1 - #{choices.size})"
402
- elsif choices[d - 1] && choices[d - 1].disabled?
403
- "default index `#{d}` matches disabled choice item"
416
+ elsif (dflt_choice = choices[d - 1]) && dflt_choice.disabled?
417
+ "default index `#{d}` matches disabled choice"
404
418
  end
405
419
 
406
420
  raise(ConfigurationError, msg) if msg
407
421
  end
408
422
  end
409
423
 
424
+ # Validate default choice name
425
+ #
426
+ # @param [String] name
427
+ # the name to verify
428
+ #
429
+ # @return [String]
430
+ #
431
+ # @api private
432
+ def validate_default_name(name)
433
+ default_choice = choices.find_by(:name, name.to_s)
434
+ if default_choice.nil?
435
+ "no choice found for the default name: #{name.inspect}"
436
+ elsif default_choice.disabled?
437
+ "default name #{name.inspect} matches disabled choice"
438
+ end
439
+ end
440
+
410
441
  # Render a selection list.
411
442
  #
412
443
  # By default the result is printed out.
@@ -5,6 +5,9 @@ require_relative "question"
5
5
  module TTY
6
6
  class Prompt
7
7
  class MaskQuestion < Question
8
+ # Names for delete keys
9
+ DELETE_KEYS = %i[backspace delete].freeze
10
+
8
11
  # Create masked question
9
12
  #
10
13
  # @param [Hash] options
@@ -27,19 +30,20 @@ module TTY
27
30
  # @api public
28
31
  def mask(char = (not_set = true))
29
32
  return @mask if not_set
33
+
30
34
  @mask = char
31
35
  end
32
36
 
33
- def keyreturn(event)
37
+ def keyreturn(_event)
34
38
  @done_masked = true
35
39
  end
36
40
 
37
- def keyenter(event)
41
+ def keyenter(_event)
38
42
  @done_masked = true
39
43
  end
40
44
 
41
45
  def keypress(event)
42
- if [:backspace, :delete].include?(event.key.name)
46
+ if DELETE_KEYS.include?(event.key.name)
43
47
  @input.chop! unless @input.empty?
44
48
  elsif event.value =~ /^[^\e\n\r]/
45
49
  @input += event.value
@@ -60,6 +60,7 @@ module TTY
60
60
  @selected.delete_at(@active - 1)
61
61
  else
62
62
  return if @max && @selected.size >= @max
63
+
63
64
  @selected.insert(@active - 1, active_choice)
64
65
  end
65
66
  end
@@ -69,6 +70,7 @@ module TTY
69
70
  # @api private
70
71
  def keyctrl_a(*)
71
72
  return if @max && @max < choices.size
73
+
72
74
  @selected = SelectedChoices.new(choices.enabled, choices.enabled_indexes)
73
75
  end
74
76
 
@@ -77,6 +79,7 @@ module TTY
77
79
  # @api private
78
80
  def keyctrl_r(*)
79
81
  return if @max && @max < choices.size
82
+
80
83
  indexes = choices.each_with_index.reduce([]) do |acc, (choice, idx)|
81
84
  acc << idx if !choice.disabled? && !@selected.include?(choice)
82
85
  acc
@@ -92,14 +95,23 @@ module TTY
92
95
  def setup_defaults
93
96
  validate_defaults
94
97
  # At this stage, @choices matches all the visible choices.
95
- default_indexes = @default.map { |d| d - 1 }
98
+ default_indexes = @default.map do |d|
99
+ if d.to_s =~ INTEGER_MATCHER
100
+ d - 1
101
+ else
102
+ choices.index(choices.find_by(:name, d.to_s))
103
+ end
104
+ end
96
105
  @selected = SelectedChoices.new(@choices.values_at(*default_indexes),
97
106
  default_indexes)
98
107
 
99
- if !@default.empty?
108
+ if @default.empty?
109
+ # no default, pick the first non-disabled choice
110
+ @active = choices.index { |choice| !choice.disabled? } + 1
111
+ elsif @default.last.to_s =~ INTEGER_MATCHER
100
112
  @active = @default.last
101
- else
102
- @active = @choices.index { |choice| !choice.disabled? } + 1
113
+ elsif default_choice = choices.find_by(:name, @default.last.to_s)
114
+ @active = choices.index(default_choice) + 1
103
115
  end
104
116
  end
105
117
 
@@ -25,6 +25,7 @@ module TTY
25
25
  # @api public
26
26
  def help(value = (not_set = true))
27
27
  return @help if not_set
28
+
28
29
  @help = value
29
30
  end
30
31
 
@@ -42,7 +43,7 @@ module TTY
42
43
  if !echo?
43
44
  header
44
45
  elsif @done
45
- header << @prompt.decorate("#{@input}", @active_color)
46
+ header << @prompt.decorate(@input.to_s, @active_color)
46
47
  elsif @first_render
47
48
  header << @prompt.decorate(help, @help_color)
48
49
  @first_render = false
@@ -55,7 +55,7 @@ module TTY
55
55
  @error_color = options.fetch(:error_color) { :red }
56
56
  @value = options.fetch(:value) { UndefinedSetting }
57
57
  @quiet = options.fetch(:quiet) { @prompt.quiet }
58
- @messages = Utils.deep_copy(options.fetch(:messages) { { } })
58
+ @messages = Utils.deep_copy(options.fetch(:messages) { {} })
59
59
  @done = false
60
60
  @first_render = true
61
61
  @input = nil
@@ -170,7 +170,7 @@ module TTY
170
170
  #
171
171
  # @api private
172
172
  def read_input(question)
173
- options = {echo: echo}
173
+ options = { echo: echo }
174
174
  if value? && @first_render
175
175
  options[:value] = @value
176
176
  @first_render = false
@@ -257,6 +257,7 @@ module TTY
257
257
  # @api public
258
258
  def default(value = (not_set = true))
259
259
  return @default if not_set
260
+
260
261
  @default = value
261
262
  end
262
263
 
@@ -277,9 +278,10 @@ module TTY
277
278
  def required(value = (not_set = true), message = nil)
278
279
  messages[:required?] = message if message
279
280
  return @required if not_set
281
+
280
282
  @required = value
281
283
  end
282
- alias_method :required?, :required
284
+ alias required? required
283
285
 
284
286
  # Set validation rule for an argument
285
287
  #
@@ -298,6 +300,7 @@ module TTY
298
300
  # @api public
299
301
  def value(val)
300
302
  return @value if val.nil?
303
+
301
304
  @value = val
302
305
  end
303
306
 
@@ -327,18 +330,20 @@ module TTY
327
330
  # @api public
328
331
  def echo(value = nil)
329
332
  return @echo if value.nil?
333
+
330
334
  @echo = value
331
335
  end
332
- alias_method :echo?, :echo
336
+ alias echo? echo
333
337
 
334
338
  # Turn raw mode on or off. This enables character-based input.
335
339
  #
336
340
  # @api public
337
341
  def raw(value = nil)
338
342
  return @raw if value.nil?
343
+
339
344
  @raw = value
340
345
  end
341
- alias_method :raw?, :raw
346
+ alias raw? raw
342
347
 
343
348
  # Set expected range of values
344
349
  #
@@ -351,6 +356,7 @@ module TTY
351
356
  @in = Converters.convert(:range, @in)
352
357
  end
353
358
  return @in if not_set
359
+
354
360
  @in = Converters.convert(:range, value)
355
361
  end
356
362
 
@@ -42,7 +42,7 @@ module TTY
42
42
  (question.in? && question.in.include?(cast(value)))
43
43
  [value]
44
44
  else
45
- tokens = {value: value, in: question.in}
45
+ tokens = { value: value, in: question.in }
46
46
  [value, question.message_for(:range?, tokens)]
47
47
  end
48
48
  end
@@ -56,7 +56,7 @@ module TTY
56
56
  Validation.new(question.validation).call(value))
57
57
  [value]
58
58
  else
59
- tokens = {valid: question.validation.inspect}
59
+ tokens = { valid: question.validation.inspect }
60
60
  [value, question.message_for(:valid?, tokens)]
61
61
  end
62
62
  end
@@ -89,7 +89,7 @@ module TTY
89
89
  if question.convert? && !Utils.blank?(value)
90
90
  result = question.convert_result(value)
91
91
  if result == Const::Undefined
92
- tokens = {value: value, type: question.convert}
92
+ tokens = { value: value, type: question.convert }
93
93
  [value, question.message_for(:convert?, tokens)]
94
94
  else
95
95
  [result]
@@ -49,6 +49,7 @@ module TTY
49
49
  # @api public
50
50
  def self.letter_case(mod, value)
51
51
  return value unless value.is_a?(String)
52
+
52
53
  case mod
53
54
  when :up, :upcase, :uppercase
54
55
  value.upcase
@@ -75,15 +76,16 @@ module TTY
75
76
  # @api public
76
77
  def self.whitespace(mod, value)
77
78
  return value unless value.is_a?(String)
79
+
78
80
  case mod
79
81
  when :trim, :strip
80
82
  value.strip
81
83
  when :chomp
82
84
  value.chomp
83
85
  when :collapse
84
- value.gsub(/\s+/, ' ')
86
+ value.gsub(/\s+/, " ")
85
87
  when :remove
86
- value.gsub(/\s+/, '')
88
+ value.gsub(/\s+/, "")
87
89
  else
88
90
  value
89
91
  end
@@ -34,6 +34,7 @@ module TTY
34
34
  # @api public
35
35
  def each(&block)
36
36
  return to_enum unless block_given?
37
+
37
38
  @selected.each(&block)
38
39
  end
39
40
 
@@ -9,7 +9,7 @@ module TTY
9
9
  class Slider
10
10
  HELP = "(Use %s arrow keys, press Enter to select)"
11
11
 
12
- FORMAT = ":slider %d"
12
+ FORMAT = ":slider %s"
13
13
 
14
14
  # Initailize a Slider
15
15
  #
@@ -26,9 +26,10 @@ module TTY
26
26
  def initialize(prompt, **options)
27
27
  @prompt = prompt
28
28
  @prefix = options.fetch(:prefix) { @prompt.prefix }
29
- @min = options.fetch(:min) { 0 }
30
- @max = options.fetch(:max) { 10 }
31
- @step = options.fetch(:step) { 1 }
29
+ @choices = Choices.new
30
+ @min = options.fetch(:min, 0)
31
+ @max = options.fetch(:max, 10)
32
+ @step = options.fetch(:step, 1)
32
33
  @default = options[:default]
33
34
  @active_color = options.fetch(:active_color) { @prompt.active_color }
34
35
  @help_color = options.fetch(:help_color) { @prompt.help_color }
@@ -60,9 +61,14 @@ module TTY
60
61
  # @api private
61
62
  def initial
62
63
  if @default.nil?
63
- range.size / 2
64
+ # no default - choose the middle option
65
+ choices.size / 2
66
+ elsif default_choice = choices.find_by(:name, @default)
67
+ # found a Choice by name - use it
68
+ choices.index(default_choice)
64
69
  else
65
- range.index(@default)
70
+ # default is the index number
71
+ @default - 1
66
72
  end
67
73
  end
68
74
 
@@ -94,15 +100,6 @@ module TTY
94
100
  @show_help = value
95
101
  end
96
102
 
97
- # Range of numbers to render
98
- #
99
- # @return [Array[Integer]]
100
- #
101
- # @api private
102
- def range
103
- (@min..@max).step(@step).to_a
104
- end
105
-
106
103
  # @api public
107
104
  def default(value)
108
105
  @default = value
@@ -123,6 +120,32 @@ module TTY
123
120
  @step = value
124
121
  end
125
122
 
123
+ # Add a single choice
124
+ #
125
+ # @api public
126
+ def choice(*value, &block)
127
+ if block
128
+ @choices << (value << block)
129
+ else
130
+ @choices << value
131
+ end
132
+ end
133
+
134
+ # Add multiple choices
135
+ #
136
+ # @param [Array[Object]] values
137
+ # the values to add as choices
138
+ #
139
+ # @api public
140
+ def choices(values = (not_set = true))
141
+ if not_set
142
+ @choices
143
+ else
144
+ values.each { |val| @choices << val }
145
+ end
146
+ end
147
+
148
+ # @api public
126
149
  def format(value)
127
150
  @format = value
128
151
  end
@@ -140,9 +163,14 @@ module TTY
140
163
  # the question to ask
141
164
  #
142
165
  # @apu public
143
- def call(question, &block)
166
+ def call(question, possibilities = nil, &block)
144
167
  @question = question
168
+ choices(possibilities) if possibilities
145
169
  block.call(self) if block
170
+ # set up a Choices collection for min, max, step
171
+ # if no possibilities were supplied
172
+ choices((@min..@max).step(@step).to_a) if @choices.empty?
173
+
146
174
  @active = initial
147
175
  @prompt.subscribe(self) do
148
176
  render
@@ -155,7 +183,7 @@ module TTY
155
183
  alias keydown keyleft
156
184
 
157
185
  def keyright(*)
158
- @active += 1 if (@active + 1) < range.size
186
+ @active += 1 if (@active + 1) < choices.size
159
187
  end
160
188
  alias keyup keyright
161
189
 
@@ -208,11 +236,11 @@ module TTY
208
236
  @prompt.print(@prompt.clear_lines(lines))
209
237
  end
210
238
 
211
- # @return [Integer]
239
+ # @return [Integer, String]
212
240
  #
213
241
  # @api private
214
242
  def answer
215
- range[@active]
243
+ choices[@active].value
216
244
  end
217
245
 
218
246
  # Render question with the slider
@@ -223,7 +251,7 @@ module TTY
223
251
  def render_question
224
252
  header = ["#{@prefix}#{@question} "]
225
253
  if @done
226
- header << @prompt.decorate(answer.to_s, @active_color)
254
+ header << @prompt.decorate(choices[@active].to_s, @active_color)
227
255
  header << "\n"
228
256
  else
229
257
  header << render_slider
@@ -244,8 +272,8 @@ module TTY
244
272
  def render_slider
245
273
  slider = (@symbols[:line] * @active) +
246
274
  @prompt.decorate(@symbols[:bullet], @active_color) +
247
- (@symbols[:line] * (range.size - @active - 1))
248
- value = range[@active]
275
+ (@symbols[:line] * (choices.size - @active - 1))
276
+ value = choices[@active].name
249
277
  case @format
250
278
  when Proc
251
279
  @format.call(slider, value)
@@ -88,6 +88,7 @@ module TTY
88
88
  # @api private
89
89
  def evaluate
90
90
  return @suggestions if @suggestions.empty?
91
+
91
92
  if @suggestions.one?
92
93
  build_single_suggestion
93
94
  else
@@ -19,7 +19,7 @@ module TTY
19
19
 
20
20
  class Test < TTY::Prompt
21
21
  def initialize(**options)
22
- @input = StringIO.new
22
+ @input = StringIO.new
23
23
  @input.extend(StringIOExtensions)
24
24
  @output = StringIO.new
25
25
 
@@ -27,7 +27,7 @@ module TTY
27
27
  input: @input,
28
28
  output: @output,
29
29
  env: { "TTY_TEST" => true },
30
- enable_color: options.fetch(:enable_color) { true }
30
+ enable_color: options.fetch(:enable_color, true)
31
31
  })
32
32
  super(**options)
33
33
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module TTY
4
4
  class Prompt
5
- VERSION = "0.22.0"
5
+ VERSION = "0.23.0"
6
6
  end # Prompt
7
7
  end # TTY
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tty-prompt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.22.0
4
+ version: 0.23.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Piotr Murach
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-07-20 00:00:00.000000000 Z
11
+ date: 2020-12-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pastel