tty-prompt 0.22.0 → 0.23.0

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.
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