tty-prompt 0.7.1 → 0.8.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
  SHA1:
3
- metadata.gz: da6d9da4a55660577a3f5c65c4058c666defd1d7
4
- data.tar.gz: 7d647c6c08f2d3271b4660e3da777885001cc772
3
+ metadata.gz: f9a360c4b3a7cd670868b20f7b220223274f555b
4
+ data.tar.gz: 17667527540e13b4e91eca3c042c9fcc5cb24b86
5
5
  SHA512:
6
- metadata.gz: 876eea0fd3fea3da77b4c2730247807022fc2553fa7475ca089d099c961824818ee59370129f10520b9a29402bbecc3b912b1ed5e5e9fb9c583643561d6aeab1
7
- data.tar.gz: 9b3a34ab5221a8305a6608b48cb1b6567cb6bfbd77e562dfd3dda540e63816978212380228570ba290e98c2e5a70f7b67a2877bb560482082040bf07cf2f4d18
6
+ metadata.gz: e7489d7dcde47bad702c66740391db047b17d94eedb9c110de0b77ffc59ba6c4cca83f95d2e5dc85417b6e1e5f409d0bd88c5a00367e260b29ea48f3efe7c798
7
+ data.tar.gz: ef5bec91d62387297bbc29b947bb39d014e3a2239d5679a9175e6944520d490caca7f327abf695c3b27f42055169c89cd7b924522dec1a55e61e94ed2e9e379b
data/.travis.yml CHANGED
@@ -6,10 +6,10 @@ bundler_args: --without tools
6
6
  script: "bundle exec rake ci"
7
7
  rvm:
8
8
  - 1.9.3
9
- - 2.0
10
- - 2.1
11
- - 2.2
12
- - 2.3.0
9
+ - 2.0.0
10
+ - 2.1.10
11
+ - 2.2.5
12
+ - 2.3.1
13
13
  - ruby-head
14
14
  - jruby-9000
15
15
  - jruby-head
@@ -18,6 +18,7 @@ matrix:
18
18
  allow_failures:
19
19
  - rvm: ruby-head
20
20
  - rvm: jruby-head
21
+ - rvm: rbx-2
21
22
  fast_finish: true
22
23
  branches:
23
24
  only: master
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Change log
2
2
 
3
+ ## [v0.8.0] - 2016-11-29
4
+
5
+ ### Added
6
+ * Add ability to publish custom key events for VIM keybindings customisations etc...
7
+
8
+ ### Fixed
9
+ * Fix Reader#read_char to use Ruby internal buffers instead of direct system call by @kke(Kimmo Lehto)
10
+ * Fix issue with #ask required & validate checks to take into account required when validating values
11
+ * Fix bug with #read_keypress to handle function keys and meta navigation keys
12
+ * Fix issue with default messages not displaying for `range`, `required` and `validate`
13
+
14
+ ## [v0.7.1] - 2016-08-07
15
+
16
+ ### Fixed
17
+ * Fix Reader::Mode to include standard io library
18
+
3
19
  ## [v0.7.0] - 2016-07-17
4
20
 
5
21
  ### Added
@@ -98,6 +114,7 @@
98
114
 
99
115
  * Initial implementation and release
100
116
 
117
+ [v0.7.1]: https://github.com/piotrmurach/tty-prompt/compare/v0.7.0...v0.7.1
101
118
  [v0.7.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.6.0...v0.7.0
102
119
  [v0.6.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.5.0...v0.6.0
103
120
  [v0.5.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.4.0...v0.5.0
data/Gemfile CHANGED
@@ -6,6 +6,7 @@ group :test do
6
6
  gem 'benchmark-ips', '~> 2.0.0'
7
7
  gem 'simplecov', '~> 0.10.0'
8
8
  gem 'coveralls', '~> 0.8.2'
9
+ gem 'term-ansicolor', '=1.3.2'
9
10
  end
10
11
 
11
12
  group :tools do
data/README.md CHANGED
@@ -51,7 +51,7 @@ Or install it yourself as:
51
51
  * [2.2.5 modify](#225-modify)
52
52
  * [2.2.6 required](#226-required)
53
53
  * [2.2.7 validate](#227-validate)
54
- * [2.2.8 messages](#228-messages)
54
+ * [2.2.8 error messages](#228-error-messages)
55
55
  * [2.2.9 prefix](#229-prefix)
56
56
  * [2.2.10 active_color](#2210-active_color)
57
57
  * [2.2.11 help_color](#2211-help_color)
@@ -71,7 +71,7 @@ Or install it yourself as:
71
71
  * [2.12.1 ok](#2121-ok)
72
72
  * [2.12.2 warn](#2122-warn)
73
73
  * [2.12.3 error](#2123-error)
74
-
74
+ * [2.13 keyboard events](#213-keyboard-events)
75
75
 
76
76
  ## 1. Usage
77
77
 
@@ -322,7 +322,7 @@ The **TTY::Prompt** comes with bult-in validations for `:email` and you can use
322
322
  prompt.ask('What is your email?') { |q| q.validate :email }
323
323
  ```
324
324
 
325
- #### 2.2.8 messages
325
+ #### 2.2.8 error messages
326
326
 
327
327
  By default `tty-prompt` comes with predefined error messages for `required`, `in`, `validate` options. You can change these and configure to your liking either by inling them with the option:
328
328
 
@@ -341,6 +341,15 @@ prompt.ask('What is your email?') do |q|
341
341
  end
342
342
  ```
343
343
 
344
+ to change default range validation error message do:
345
+
346
+ ```ruby
347
+ prompt.ask('How spicy on scale (1-5)? ') do |q|
348
+ q.in '1-5'
349
+ q.messages[:range?] = '%{value} out of expected range #{in}'
350
+ end
351
+ ```
352
+
344
353
  #### 2.2.9 prefix
345
354
 
346
355
  You can prefix each question asked using the `:prefix` option. This option can be applied either globally for all prompts or individual for each one:
@@ -906,6 +915,58 @@ Print message(s) in red do:
906
915
  prompt.error(...)
907
916
  ```
908
917
 
918
+ #### 2.13 keyboard events
919
+
920
+ All the prompt types, when a key is pressed, fire key press events. You can subscribe to listen to this events by calling `on` with type of event name.
921
+
922
+ ```ruby
923
+ prompt.on(:keypress) { |event| ... }
924
+ ```
925
+
926
+ The event object is yielded to a block whenever particular event fires. The event has `key` and `value` methods. Further, the `key` responds to following messages:
927
+
928
+ * `name` - the name of the event such as :up, :down, letter or digit
929
+ * `meta` - true if event is non-standard key associated
930
+ * `shift` - true if shift has been pressed with the key
931
+ * `ctrl` - true if ctrl has been pressed with the key
932
+
933
+ For example, to add vim like key navigation to `select` prompt one would do the following:
934
+
935
+ ```ruby
936
+ prompt.on(:keypress) do |event|
937
+ if event.key.name == 'j'
938
+ prompt.publish(:keydown)
939
+ end
940
+
941
+ if event.key.name == 'k'
942
+ prompt.publish(:keyup)
943
+ end
944
+ end
945
+ ```
946
+
947
+ You can subscribe to more than one event:
948
+
949
+ ```ruby
950
+ prompt.on(:keypress) { |key| ... }
951
+ .on(:keydown) { |key| ... }
952
+ ```
953
+
954
+ The available events are:
955
+
956
+ * `:keypress`
957
+ * `:keydown`
958
+ * `:keyup`
959
+ * `:keyleft`
960
+ * `:keyright`
961
+ * `:keynum`
962
+ * `:keytab`
963
+ * `:keyenter`
964
+ * `:keyreturn`
965
+ * `:keyspace`
966
+ * `:keyescape`
967
+ * `:keydelete`
968
+ * `:keybackspace`
969
+
909
970
  ## Contributing
910
971
 
911
972
  1. Fork it ( https://github.com/piotrmurach/tty-prompt/fork )
data/examples/ask.rb CHANGED
@@ -5,3 +5,11 @@ require 'tty-prompt'
5
5
  prompt = TTY::Prompt.new
6
6
 
7
7
  prompt.ask('What is your name?', default: ENV['USER'])
8
+
9
+ prompt.ask('Folder name?') do |q|
10
+ q.required(true)
11
+ q.validate ->(v) { return !Dir.exist?(v) }
12
+ q.messages[:valid?] = 'Folder already exists?'
13
+ q.messages[:required?] = 'Folder name must not be empty'
14
+ end
15
+
@@ -0,0 +1,11 @@
1
+ # encoding: utf-8
2
+
3
+ require 'tty-prompt'
4
+
5
+ prompt = TTY::Prompt::new(interrupt: :exit)
6
+
7
+ prompt.on(:keypress) do |event|
8
+ puts "name: #{event.key.name}, value: #{event.value.dump}"
9
+ end
10
+
11
+ prompt.read_keypress
data/examples/select.rb CHANGED
@@ -5,4 +5,14 @@ require 'tty-prompt'
5
5
  prompt = TTY::Prompt.new
6
6
 
7
7
  warriors = %w(Scorpion Kano Jax)
8
+
9
+ prompt.on(:keypress) do |event|
10
+ if event.key.name == 'j'
11
+ prompt.publish(:keydown)
12
+ end
13
+ if event.key.name == 'k'
14
+ prompt.publish(:keyup)
15
+ end
16
+ end
17
+
8
18
  prompt.select('Choose your destiny?', warriors)
data/lib/tty/prompt.rb CHANGED
@@ -49,8 +49,8 @@ module TTY
49
49
  def_delegators :@cursor, :clear_lines, :clear_line,
50
50
  :show, :hide
51
51
 
52
- def_delegators :@reader, :read_line, :read_keypress,
53
- :read_multiline, :on, :subscribe
52
+ def_delegators :@reader, :read_char, :read_line, :read_keypress,
53
+ :read_multiline, :on, :subscribe, :publish
54
54
 
55
55
  def_delegators :@output, :print, :puts, :flush
56
56
 
@@ -98,7 +98,7 @@ module TTY
98
98
  # @api public
99
99
  def ask(message, *args, &block)
100
100
  options = Utils.extract_options!(args)
101
- options.merge!(self.class.messages)
101
+ options.merge!({messages: self.class.messages})
102
102
  question = Question.new(self, options)
103
103
  question.call(message, &block)
104
104
  end
@@ -49,7 +49,7 @@ module TTY
49
49
  # Check if input requires validation
50
50
  class CheckValidation
51
51
  def self.call(question, value)
52
- if !question.validation? ||
52
+ if !question.validation? || (question.required? && value.nil?) ||
53
53
  (question.validation? &&
54
54
  Validation.new(question.validation).call(value))
55
55
  [value]
@@ -86,15 +86,18 @@ module TTY
86
86
 
87
87
  # Reads single character including invisible multibyte codes
88
88
  #
89
+ # @params [Integer] bytes
90
+ # the number of bytes to read
91
+ #
89
92
  # @return [String]
90
93
  #
91
94
  # @api public
92
- def read_char
93
- chars = input.sysread(1)
95
+ def read_char(bytes = 1)
96
+ chars = input.readpartial(bytes)
94
97
  while CSI.start_with?(chars) ||
95
98
  chars.start_with?(CSI) &&
96
99
  !(64..126).include?(chars.each_codepoint.to_a.last)
97
- next_char = read_char
100
+ next_char = read_char(bytes + 1)
98
101
  chars << next_char
99
102
  end
100
103
  chars
@@ -4,7 +4,7 @@ module TTY
4
4
  class Prompt
5
5
  class Reader
6
6
  module Codes
7
- BACKSPACE = "\177"
7
+ BACKSPACE = "\x7f"
8
8
  DELETE = "\004"
9
9
  ESCAPE = "\e"
10
10
  LINEFEED = "\n"
@@ -12,23 +12,35 @@ module TTY
12
12
  SPACE = " "
13
13
  TAB = "\t"
14
14
 
15
- KEY_UP = "\e[A"
16
- KEY_DOWN = "\e[B"
17
- KEY_RIGHT = "\e[C"
18
- KEY_LEFT = "\e[D"
19
- KEY_CLEAR = "\e[E"
20
- KEY_END = "\e[F"
21
- KEY_HOME = "\e[H"
22
- KEY_DELETE = "\e[3"
15
+ KEY_UP = "[A"
16
+ KEY_DOWN = "[B"
17
+ KEY_RIGHT = "[C"
18
+ KEY_LEFT = "[D"
19
+ KEY_CLEAR = "[E"
20
+ KEY_END = "[F"
21
+ KEY_HOME = "[H"
22
+ KEY_DELETE = "[3"
23
23
 
24
- KEY_UP_ALT = "\eOA"
25
- KEY_DOWN_ALT = "\eOB"
26
- KEY_RIGHT_ALT = "\eOC"
27
- KEY_LEFT_ALT = "\eOD"
28
- KEY_CLEAR_ALT = "\eOE"
29
- KEY_END_ALT = "\eOF"
30
- KEY_HOME_ALT = "\eOH"
31
- KEY_DELETE_ALT = "\eO3"
24
+ KEY_UP_XTERM = "OA"
25
+ KEY_DOWN_XTERM = "OB"
26
+ KEY_RIGHT_XTERM = "OC"
27
+ KEY_LEFT_XTERM = "OD"
28
+ KEY_CLEAR_XTERM = "OE"
29
+ KEY_END_XTERM = "OF"
30
+ KEY_HOME_XTERM = "OH"
31
+ KEY_DELETE_XTERM = "O3"
32
+
33
+ KEY_UP_SHIFT = "[a"
34
+ KEY_DOWN_SHIFT = "[b"
35
+ KEY_RIGHT_SHIFT = "[c"
36
+ KEY_LEFT_SHIFT = "[d"
37
+ KEY_CLEAR_SHIFT = "[e"
38
+
39
+ KEY_UP_CTRL = "0a"
40
+ KEY_DOWN_CTRL = "0b"
41
+ KEY_RIGHT_CTRL = "0c"
42
+ KEY_LEFT_CTRL = "0d"
43
+ KEY_CLEAR_CTRL = "0e"
32
44
 
33
45
  CTRL_J = "\x0A"
34
46
  CTRL_N = "\x0E"
@@ -39,30 +51,30 @@ module TTY
39
51
  CTRL_H = "\b"
40
52
  CTRL_L = "\f"
41
53
 
42
- F1_XTERM = "\eOP"
43
- F2_XTERM = "\eOQ"
44
- F3_XTERM = "\eOR"
45
- F4_XTERM = "\eOS"
54
+ F1_XTERM = "OP"
55
+ F2_XTERM = "OQ"
56
+ F3_XTERM = "OR"
57
+ F4_XTERM = "OS"
46
58
 
47
- F1_GNOME = "\e[11~"
48
- F2_GNOME = "\e[12~"
49
- F3_GNOME = "\e[13~"
50
- F4_GNOME = "\e[14~"
59
+ F1_GNOME = "[11~"
60
+ F2_GNOME = "[12~"
61
+ F3_GNOME = "[13~"
62
+ F4_GNOME = "[14~"
51
63
 
52
- F1_WIN = "\e[[A"
53
- F2_WIN = "\e[[B"
54
- F3_WIN = "\e[[C"
55
- F4_WIN = "\e[[D"
56
- F5_WIN = "\e[[E"
64
+ F1_WIN = "[[A"
65
+ F2_WIN = "[[B"
66
+ F3_WIN = "[[C"
67
+ F4_WIN = "[[D"
68
+ F5_WIN = "[[E"
57
69
 
58
- F5 = "\e[15~"
59
- F6 = "\e[17~"
60
- F7 = "\e[18~"
61
- F8 = "\e[19~"
62
- F9 = "\e[20~"
63
- F10 = "\e[21~"
64
- F11 = "\e[23~"
65
- F12 = "\e[24~"
70
+ F5 = "[15~"
71
+ F6 = "[17~"
72
+ F7 = "[18~"
73
+ F8 = "[19~"
74
+ F9 = "[20~"
75
+ F10 = "[21~"
76
+ F11 = "[23~"
77
+ F12 = "[24~"
66
78
  end # Codes
67
79
  end # Reader
68
80
  end # Prompt
@@ -21,10 +21,11 @@ module TTY
21
21
  #
22
22
  # @api public
23
23
  class KeyEvent < Struct.new(:value, :key)
24
- META_KEY_CODE_RE = /^(?:\e)(O|N|\[|\[\[)(?:(\d+)(?:;(\d+))?([~^$])|(?:1;)?(\d+)?([a-zA-Z]))/
24
+ META_KEY_CODE_RE = /^(?:\e+)(O|N|\[|\[\[)(?:(\d+)(?:;(\d+))?([~^$])|(?:1;)?(\d+)?([a-zA-Z]))/
25
25
 
26
26
  def self.from(char)
27
27
  key = Key.new
28
+
28
29
  case char
29
30
  when Codes::RETURN then key.name = :return
30
31
  when Codes::LINEFEED then key.name = :enter
@@ -33,9 +34,14 @@ module TTY
33
34
  "#{Codes::ESCAPE}#{Codes::BACKSPACE}",
34
35
  "#{Codes::ESCAPE}#{Codes::CTRL_H}"
35
36
  key.name = :backspace
37
+ key.meta = (char.chars.to_a[0] == Codes::ESCAPE)
36
38
  when Codes::DELETE then key.name = :delete
37
- when Codes::SPACE then key.name = :space
38
- when Codes::CTRL_C, Codes::ESCAPE then key.name = :escape
39
+ when Codes::SPACE, "#{Codes::ESCAPE}#{Codes::SPACE}"
40
+ key.name = :space
41
+ key.meta = (char.size == 2)
42
+ when Codes::ESCAPE, "#{Codes::ESCAPE}#{Codes::ESCAPE}"
43
+ key.name = :escape
44
+ key.meta = (char.size == 2)
39
45
  when proc { |c| c.length == 1 && c =~ /[a-z]/ }
40
46
  key.name = char
41
47
  when proc { |c| c.length == 1 && c =~ /[A-Z]/ }
@@ -44,15 +50,21 @@ module TTY
44
50
  when /^\d+$/
45
51
  key.name = :num
46
52
  when META_KEY_CODE_RE # ansi escape
47
- key.meta = true
53
+ parts = META_KEY_CODE_RE.match(char)
54
+ code = "#{parts[1]}#{parts[2]}#{parts[4]}#{parts[6]}"
55
+ modifier = (parts[3] || parts[5] || 1) - 1
56
+
57
+ key.ctrl = (modifier & 4) != 0
58
+ key.meta = (modifier & 10) != 0
59
+ key.shift = (modifier & 1) != 0
48
60
 
49
- case char
61
+ case code
50
62
  # f1 - f12
51
63
  when Codes::F1_XTERM, Codes::F1_GNOME, Codes::F1_WIN then key.name = :f1
52
64
  when Codes::F2_XTERM, Codes::F2_GNOME, Codes::F2_WIN then key.name = :f2
53
65
  when Codes::F3_XTERM, Codes::F3_GNOME, Codes::F3_WIN then key.name = :f3
54
66
  when Codes::F4_XTERM, Codes::F4_GNOME, Codes::F4_WIN then key.name = :f4
55
- when Codes::F5 then key.name = :f5
67
+ when Codes::F5, Codes::F5_WIN then key.name = :f5
56
68
  when Codes::F6 then key.name = :f6
57
69
  when Codes::F7 then key.name = :f7
58
70
  when Codes::F8 then key.name = :f8
@@ -61,19 +73,37 @@ module TTY
61
73
  when Codes::F11 then key.name = :f11
62
74
  when Codes::F12 then key.name = :f12
63
75
  # navigation
64
- when Codes::KEY_UP, Codes::KEY_UP_ALT, Codes::CTRL_K, Codes::CTRL_P
76
+ when Codes::KEY_UP, Codes::KEY_UP_XTERM,
77
+ Codes::CTRL_K, Codes::CTRL_P
65
78
  key.name = :up
66
- when Codes::KEY_DOWN, Codes::KEY_DOWN_ALT, Codes::CTRL_J, Codes::CTRL_N
79
+ when Codes::KEY_UP_SHIFT then key.name = :up; key.shift = true
80
+ when Codes::KEY_UP_CTRL then key.name = :up; key.ctrl = true
81
+
82
+ when Codes::KEY_DOWN, Codes::KEY_DOWN_XTERM,
83
+ Codes::CTRL_J, Codes::CTRL_N
67
84
  key.name = :down
68
- when Codes::KEY_RIGHT, Codes::KEY_RIGHT_ALT, Codes::CTRL_L
85
+ when Codes::KEY_DOWN_SHIFT then key.name = :down; key.shift = true
86
+ when Codes::KEY_DOWN_CTRL then key.name = :down; key.ctrl = true
87
+
88
+ when Codes::KEY_RIGHT, Codes::KEY_RIGHT_XTERM, Codes::CTRL_L
69
89
  key.name = :right
70
- when Codes::KEY_LEFT, Codes::KEY_LEFT_ALT, Codes::CTRL_H
90
+ when Codes::KEY_RIGHT_SHIFT then key.name = :right; key.shift = true
91
+ when Codes::KEY_RIGHT_CTRL then key.name = :right; key.ctrl = true
92
+
93
+ when Codes::KEY_LEFT, Codes::KEY_LEFT_XTERM, Codes::CTRL_H
71
94
  key.name = :left
72
- when Codes::KEY_CLEAR, Codes::KEY_CLEAR_ALT
95
+ when Codes::KEY_LEFT_SHIFT then key.name = :left; key.shift = true
96
+ when Codes::KEY_LEFT_CTRL then key.name = :left; key.ctrl = true
97
+
98
+ when Codes::KEY_CLEAR, Codes::KEY_CLEAR_XTERM
73
99
  key.name = :clear
74
- when Codes::KEY_END, Codes::KEY_END_ALT
100
+ when Codes::KEY_CLEAR_SHIFT then key.name = :clear; key.shift = true
101
+ when Codes::KEY_CLEAR_CTRL then key.name = :clear; key.ctrl = true
102
+
103
+ when Codes::KEY_END, Codes::KEY_END_XTERM
75
104
  key.name = :end
76
- when Codes::KEY_HOME, Codes::KEY_HOME_ALT
105
+
106
+ when Codes::KEY_HOME, Codes::KEY_HOME_XTERM
77
107
  key.name = :home
78
108
  end
79
109
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module TTY
4
4
  class Prompt
5
- VERSION = "0.7.1"
5
+ VERSION = "0.8.0"
6
6
  end # Prompt
7
7
  end # TTY
@@ -7,16 +7,20 @@ RSpec.describe TTY::Prompt::Question, '#in' do
7
7
  it "reads range from option" do
8
8
  prompt.input << '8'
9
9
  prompt.input.rewind
10
+
10
11
  answer = prompt.ask("How do you like it on scale 1-10?", in: '1-10')
12
+
11
13
  expect(answer).to eq('8')
12
14
  end
13
15
 
14
16
  it 'reads number within string range' do
15
17
  prompt.input << '8'
16
18
  prompt.input.rewind
19
+
17
20
  answer = prompt.ask("How do you like it on scale 1-10?") do |q|
18
21
  q.in('1-10')
19
22
  end
23
+
20
24
  expect(answer).to eq('8')
21
25
  expect(prompt.output.string).to eq([
22
26
  "How do you like it on scale 1-10? ",
@@ -29,9 +33,11 @@ RSpec.describe TTY::Prompt::Question, '#in' do
29
33
  it 'reads number within digit range' do
30
34
  prompt.input << '8.1'
31
35
  prompt.input.rewind
36
+
32
37
  answer = prompt.ask("How do you like it on scale 1-10?") do |q|
33
38
  q.in(1.0..11.5)
34
39
  end
40
+
35
41
  expect(answer).to eq('8.1')
36
42
  expect(prompt.output.string).to eq([
37
43
  "How do you like it on scale 1-10? ",
@@ -44,9 +50,11 @@ RSpec.describe TTY::Prompt::Question, '#in' do
44
50
  it 'reads letters within range' do
45
51
  prompt.input << 'E'
46
52
  prompt.input.rewind
53
+
47
54
  answer = prompt.ask("Your favourite vitamin? (A-K)") do |q|
48
55
  q.in('A-K')
49
56
  end
57
+
50
58
  expect(answer).to eq('E')
51
59
  expect(prompt.output.string).to eq([
52
60
  "Your favourite vitamin? (A-K) ",
@@ -55,4 +63,45 @@ RSpec.describe TTY::Prompt::Question, '#in' do
55
63
  "Your favourite vitamin? (A-K) \e[32mE\e[0m\n"
56
64
  ].join)
57
65
  end
66
+
67
+ it "provides default error message when wrong input" do
68
+ prompt.input << "A\n2\n"
69
+ prompt.input.rewind
70
+
71
+ answer = prompt.ask("How spicy on scale? (1-5)", in: '1-5')
72
+
73
+ expect(answer).to eq('2')
74
+ expect(prompt.output.string).to eq([
75
+ "How spicy on scale? (1-5) ",
76
+ "\e[1000D\e[K",
77
+ "\e[31m>>\e[0m Value A must be within the range 1..5\e[1A",
78
+ "\e[1000D\e[K",
79
+ "How spicy on scale? (1-5) ",
80
+ "\e[1000D\e[K\e[1A",
81
+ "\e[1000D\e[K",
82
+ "How spicy on scale? (1-5) \e[32m2\e[0m\n"
83
+ ].join)
84
+ end
85
+
86
+ it "overwrites default error message when wrong input" do
87
+ prompt.input << "A\n2\n"
88
+ prompt.input.rewind
89
+
90
+ answer = prompt.ask("How spicy on scale? (1-5)") do |q|
91
+ q.in '1-5'
92
+ q.messages[:range?] = 'Ohh dear what is this %{value} doing in %{in}?'
93
+ end
94
+
95
+ expect(answer).to eq('2')
96
+ expect(prompt.output.string).to eq([
97
+ "How spicy on scale? (1-5) ",
98
+ "\e[1000D\e[K",
99
+ "\e[31m>>\e[0m Ohh dear what is this A doing in 1..5?\e[1A",
100
+ "\e[1000D\e[K",
101
+ "How spicy on scale? (1-5) ",
102
+ "\e[1000D\e[K\e[1A",
103
+ "\e[1000D\e[K",
104
+ "How spicy on scale? (1-5) \e[32m2\e[0m\n"
105
+ ].join)
106
+ end
58
107
  end
@@ -17,10 +17,14 @@ RSpec.describe TTY::Prompt::Question, '#required' do
17
17
  end
18
18
 
19
19
  it 'requires value to be present with option' do
20
- prompt.input << "Piotr"
20
+ prompt.input << " \nPiotr"
21
21
  prompt.input.rewind
22
- prompt.ask('What is your name?') { |q| q.required(true) }
22
+ prompt.ask('What is your name?', required: true)
23
23
  expect(prompt.output.string).to eq([
24
+ "What is your name? ",
25
+ "\e[1000D\e[K",
26
+ "\e[31m>>\e[0m Value must be provided\e[1A",
27
+ "\e[1000D\e[K",
24
28
  "What is your name? ",
25
29
  "\e[1000D\e[K\e[1A",
26
30
  "\e[1000D\e[K",
@@ -34,4 +38,30 @@ RSpec.describe TTY::Prompt::Question, '#required' do
34
38
  answer = prompt.ask('What is your name?') { |q| q.required(false) }
35
39
  expect(answer).to be_nil
36
40
  end
41
+
42
+ it "uses required in validation check" do
43
+ prompt.input << " \n#{__FILE__}\ntest\n"
44
+ prompt.input.rewind
45
+ answer = prompt.ask('File name?') do |q|
46
+ q.required(true)
47
+ q.validate { |v| !File.exist?(v) }
48
+ q.messages[:required?] = 'File name must not be empty!'
49
+ q.messages[:valid?] = 'File already exists!'
50
+ end
51
+ expect(prompt.output.string).to eq([
52
+ "File name? ",
53
+ "\e[1000D\e[K",
54
+ "\e[31m>>\e[0m File name must not be empty!",
55
+ "\e[1A\e[1000D\e[K",
56
+ "File name? ",
57
+ "\e[1000D\e[K",
58
+ "\e[31m>>\e[0m File already exists!",
59
+ "\e[1A\e[1000D\e[K",
60
+ "File name? ",
61
+ "\e[1000D\e[K",
62
+ "\e[1A\e[1000D\e[K",
63
+ "File name? \e[32mtest\e[0m\n",
64
+ ].join)
65
+ expect(answer).to eq('test')
66
+ end
37
67
  end
@@ -7,9 +7,11 @@ RSpec.describe TTY::Prompt::Question, '#validate' do
7
7
  it 'validates input with regex' do
8
8
  prompt.input << 'piotr.murach'
9
9
  prompt.input.rewind
10
+
10
11
  answer = prompt.ask('What is your username?') do |q|
11
12
  q.validate(/^[^\.]+\.[^\.]+/)
12
13
  end
14
+
13
15
  expect(answer).to eq('piotr.murach')
14
16
  expect(prompt.output.string).to eq([
15
17
  "What is your username? ",
@@ -22,18 +24,65 @@ RSpec.describe TTY::Prompt::Question, '#validate' do
22
24
  it 'validates input with proc' do
23
25
  prompt.input << 'piotr.murach'
24
26
  prompt.input.rewind
27
+
25
28
  answer = prompt.ask('What is your username?') do |q|
26
29
  q.validate { |input| input =~ /^[^\.]+\.[^\.]+/ }
27
30
  end
31
+
28
32
  expect(answer).to eq('piotr.murach')
29
33
  end
30
34
 
31
35
  it 'understands custom validation like :email' do
32
36
  prompt.input << 'piotr@example.com'
33
37
  prompt.input.rewind
38
+
39
+ answer = prompt.ask('What is your email?') do |q|
40
+ q.validate :email
41
+ end
42
+
43
+ expect(answer).to eq('piotr@example.com')
44
+ end
45
+
46
+ it "provides default error message for wrong input" do
47
+ prompt.input << "invalid\npiotr@example.com"
48
+ prompt.input.rewind
49
+
50
+ answer = prompt.ask('What is your email?') do |q|
51
+ q.validate :email
52
+ end
53
+
54
+ expect(answer).to eq('piotr@example.com')
55
+ expect(prompt.output.string).to eq([
56
+ "What is your email? ",
57
+ "\e[1000D\e[K",
58
+ "\e[31m>>\e[0m Your answer is invalid (must match :email)\e[1A",
59
+ "\e[1000D\e[K",
60
+ "What is your email? ",
61
+ "\e[1000D\e[K\e[1A",
62
+ "\e[1000D\e[K",
63
+ "What is your email? \e[32mpiotr@example.com\e[0m\n"
64
+ ].join)
65
+ end
66
+
67
+ it "provides custom error message for wrong input" do
68
+ prompt.input << "invalid\npiotr@example.com"
69
+ prompt.input.rewind
70
+
34
71
  answer = prompt.ask('What is your email?') do |q|
35
72
  q.validate :email
73
+ q.messages[:valid?] = 'Not an email!'
36
74
  end
75
+
37
76
  expect(answer).to eq('piotr@example.com')
77
+ expect(prompt.output.string).to eq([
78
+ "What is your email? ",
79
+ "\e[1000D\e[K",
80
+ "\e[31m>>\e[0m Not an email!\e[1A",
81
+ "\e[1000D\e[K",
82
+ "What is your email? ",
83
+ "\e[1000D\e[K\e[1A",
84
+ "\e[1000D\e[K",
85
+ "What is your email? \e[32mpiotr@example.com\e[0m\n"
86
+ ].join)
38
87
  end
39
88
  end
@@ -1,13 +1,21 @@
1
1
  # encoding: utf-8
2
2
 
3
- RSpec.describe TTY::Prompt::Reader::KeyEvent, '::from' do
3
+ require 'shellwords'
4
4
 
5
+ RSpec.describe TTY::Prompt::Reader::KeyEvent, '#from' do
5
6
  it "parses ctrl+h" do
6
7
  event = described_class.from("\b")
7
8
  expect(event.key.name).to eq(:backspace)
8
9
  expect(event.value).to eq("\b")
9
10
  end
10
11
 
12
+ it "parses backspace" do
13
+ event = described_class.from("\e\x7f")
14
+ expect(event.key.name).to eq(:backspace)
15
+ expect(event.key.meta).to eq(true)
16
+ expect(event.value).to eq("\e\x7f")
17
+ end
18
+
11
19
  it "parses lowercase char" do
12
20
  event = described_class.from('a')
13
21
  expect(event.key.name).to eq('a')
@@ -20,48 +28,69 @@ RSpec.describe TTY::Prompt::Reader::KeyEvent, '::from' do
20
28
  expect(event.value).to eq('A')
21
29
  end
22
30
 
23
- it "parses f5 key" do
24
- event = described_class.from("\e[15~")
25
- expect(event.key.name).to eq(:f5)
26
- end
27
-
28
- it "parses up key" do
29
- event = described_class.from("\e[A")
30
- expect(event.key.name).to eq(:up)
31
- end
32
-
33
- it "parses up key on gnome" do
34
- event = described_class.from("\eOA")
35
- expect(event.key.name).to eq(:up)
36
- end
37
-
38
- it "parses down key" do
39
- event = described_class.from("\e[B")
40
- expect(event.key.name).to eq(:down)
41
- end
42
-
43
- it "parses right key" do
44
- event = described_class.from("\e[C")
45
- expect(event.key.name).to eq(:right)
46
- end
47
-
48
- it "parses left key" do
49
- event = described_class.from("\e[D")
50
- expect(event.key.name).to eq(:left)
51
- end
52
-
53
- it "parses clear key" do
54
- event = described_class.from("\e[E")
55
- expect(event.key.name).to eq(:clear)
31
+ # F1-F12 keys
32
+ {
33
+ f1: ["\eOP", "\e[11~", "\e[[A"],
34
+ f2: ["\eOQ", "\e[12~", "\e[[B"],
35
+ f3: ["\eOR", "\e[13~", "\e[[C"],
36
+ f4: ["\eOS", "\e[14~", "\e[[D"],
37
+ f5: [ "\e[15~", "\e[[E"],
38
+ f6: [ "\e[17~" ],
39
+ f7: [ "\e[18~" ],
40
+ f8: [ "\e[19~" ],
41
+ f9: [ "\e[20~" ],
42
+ f10: [ "\e[21~" ],
43
+ f11: [ "\e[23~" ],
44
+ f12: [ "\e[24~" ]
45
+ }.each do |name, codes|
46
+ codes.each do |code|
47
+ it "parses #{Shellwords.escape(code)} as #{name} key" do
48
+ event = described_class.from(code)
49
+ expect(event.key.name).to eq(name)
50
+ expect(event.key.meta).to eq(false)
51
+ expect(event.key.ctrl).to eq(false)
52
+ expect(event.key.shift).to eq(false)
53
+ end
54
+ end
56
55
  end
57
56
 
58
- it "parses end key" do
59
- event = described_class.from("\e[F")
60
- expect(event.key.name).to eq(:end)
57
+ # arrow keys & page navigation
58
+ #
59
+ {
60
+ up: ["\e[A", "\eOA"],
61
+ down: ["\e[B", "\eOB"],
62
+ right: ["\e[C", "\eOC"],
63
+ left: ["\e[D", "\eOD"],
64
+ clear: ["\e[E", "\eOE"],
65
+ end: ["\e[F"],
66
+ home: ["\e[H"]
67
+ }.each do |name, codes|
68
+ codes.each do |code|
69
+ it "parses #{Shellwords.escape(code)} as #{name} key" do
70
+ event = described_class.from(code)
71
+ expect(event.key.name).to eq(name)
72
+ expect(event.key.meta).to eq(false)
73
+ expect(event.key.ctrl).to eq(false)
74
+ expect(event.key.shift).to eq(false)
75
+ end
76
+ end
61
77
  end
62
78
 
63
- it "parses home key" do
64
- event = described_class.from("\e[H")
65
- expect(event.key.name).to eq(:home)
79
+ {
80
+ up: ["\e[a"],
81
+ down: ["\e[b"],
82
+ right: ["\e[c"],
83
+ left: ["\e[d"],
84
+ clear: ["\e[e"],
85
+ }.each do |name, codes|
86
+ codes.each do |code|
87
+ it "parses #{Shellwords.escape(code)} as SHIFT + #{name} key" do
88
+ event = described_class.from(code)
89
+ expect(event.key.name).to eq(name)
90
+ expect(event.key.meta).to eq(false)
91
+ expect(event.key.ctrl).to eq(false)
92
+ expect(event.key.shift).to eq(true)
93
+ end
94
+ end
66
95
  end
67
96
  end
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.7.1
4
+ version: 0.8.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: 2016-08-07 00:00:00.000000000 Z
11
+ date: 2016-11-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: necromancer
@@ -139,6 +139,7 @@ files:
139
139
  - examples/enum_select.rb
140
140
  - examples/expand.rb
141
141
  - examples/in.rb
142
+ - examples/keypress.rb
142
143
  - examples/mask.rb
143
144
  - examples/multi_select.rb
144
145
  - examples/select.rb