tty-prompt 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +4 -1
  3. data/CHANGELOG.md +15 -0
  4. data/Gemfile +2 -2
  5. data/README.md +185 -25
  6. data/examples/enum.rb +8 -0
  7. data/examples/enum_select.rb +7 -0
  8. data/examples/in.rb +3 -1
  9. data/examples/slider.rb +6 -0
  10. data/lib/tty-prompt.rb +8 -2
  11. data/lib/tty/prompt.rb +63 -13
  12. data/lib/tty/prompt/converters.rb +12 -6
  13. data/lib/tty/prompt/enum_list.rb +222 -0
  14. data/lib/tty/prompt/list.rb +48 -15
  15. data/lib/tty/prompt/multi_list.rb +11 -11
  16. data/lib/tty/prompt/question.rb +38 -14
  17. data/lib/tty/prompt/question/checks.rb +5 -3
  18. data/lib/tty/prompt/reader.rb +12 -18
  19. data/lib/tty/prompt/reader/codes.rb +15 -9
  20. data/lib/tty/prompt/reader/key_event.rb +51 -24
  21. data/lib/tty/prompt/slider.rb +170 -0
  22. data/lib/tty/prompt/symbols.rb +7 -1
  23. data/lib/tty/prompt/utils.rb +31 -3
  24. data/lib/tty/prompt/version.rb +1 -1
  25. data/spec/spec_helper.rb +1 -0
  26. data/spec/unit/converters/convert_bool_spec.rb +1 -1
  27. data/spec/unit/converters/convert_date_spec.rb +11 -2
  28. data/spec/unit/converters/convert_file_spec.rb +1 -1
  29. data/spec/unit/converters/convert_number_spec.rb +19 -2
  30. data/spec/unit/converters/convert_path_spec.rb +1 -1
  31. data/spec/unit/converters/convert_range_spec.rb +4 -3
  32. data/spec/unit/enum_select_spec.rb +93 -0
  33. data/spec/unit/multi_select_spec.rb +14 -12
  34. data/spec/unit/question/checks_spec.rb +97 -0
  35. data/spec/unit/reader/key_event_spec.rb +67 -0
  36. data/spec/unit/select_spec.rb +15 -16
  37. data/spec/unit/slider_spec.rb +54 -0
  38. data/tty-prompt.gemspec +2 -1
  39. metadata +31 -5
  40. data/.ruby-version +0 -1
@@ -3,11 +3,17 @@
3
3
  module TTY
4
4
  class Prompt
5
5
  module Symbols
6
- SPACE = " "
6
+ SPACE = " "
7
+ SUCCESS = "✓"
8
+ FAILURE = "✘"
9
+
7
10
  ITEM_SECURE = "•"
8
11
  ITEM_SELECTED = "‣"
9
12
  RADIO_CHECKED = "⬢"
10
13
  RADIO_UNCHECKED = "⬡"
14
+ SLIDER_HANDLE = 'O'
15
+ SLIDER_RANGE = '-'
16
+ SLIDER_END = '|'
11
17
  end # Symbols
12
18
  end # Prompt
13
19
  end # TTY
@@ -4,13 +4,41 @@ module TTY
4
4
  module Utils
5
5
  module_function
6
6
 
7
- def extract_options!(args)
8
- args.last.respond_to?(:to_hash) ? args.pop : {}
9
- end
7
+ BLANK_REGEX = /\A[[:space:]]*\z/o.freeze
10
8
 
9
+ # Extract options hash from array argument
10
+ #
11
+ # @param [Array[Object]] args
12
+ #
13
+ # @api public
11
14
  def extract_options(args)
12
15
  options = args.last
13
16
  options.respond_to?(:to_hash) ? options.to_hash.dup : {}
14
17
  end
18
+
19
+ def extract_options!(args)
20
+ args.last.respond_to?(:to_hash) ? args.pop : {}
21
+ end
22
+
23
+ # Check if value is nil or an empty string
24
+ #
25
+ # @param [Object] value
26
+ # the value to check
27
+ #
28
+ # @return [Boolean]
29
+ #
30
+ # @api public
31
+ def blank?(value)
32
+ value.nil? ||
33
+ value.respond_to?(:empty?) && value.empty? ||
34
+ BLANK_REGEX === value
35
+ end
36
+
37
+ # Deep copy object
38
+ #
39
+ # @api public
40
+ def deep_copy(object)
41
+ Marshal.load(Marshal.dump(object))
42
+ end
15
43
  end # Utils
16
44
  end # TTY
@@ -2,6 +2,6 @@
2
2
 
3
3
  module TTY
4
4
  class Prompt
5
- VERSION = "0.3.0"
5
+ VERSION = "0.4.0"
6
6
  end # Prompt
7
7
  end # TTY
data/spec/spec_helper.rb CHANGED
@@ -16,6 +16,7 @@ if RUBY_VERSION > '1.9' and (ENV['COVERAGE'] || ENV['TRAVIS'])
16
16
  end
17
17
 
18
18
  require 'tty-prompt'
19
+ require 'rspec/mocks/matchers/have_received'
19
20
 
20
21
  RSpec.configure do |config|
21
22
  config.expect_with :rspec do |expectations|
@@ -9,7 +9,7 @@ RSpec.describe TTY::Prompt::Question, 'convert bool' do
9
9
  prompt.input.rewind
10
10
  expect {
11
11
  prompt.ask("Do you read books?", convert: :bool)
12
- }.to raise_error(Necromancer::ConversionTypeError)
12
+ }.to raise_error(TTY::Prompt::ConversionError)
13
13
  end
14
14
 
15
15
  it "handles default values" do
@@ -1,8 +1,18 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  RSpec.describe TTY::Prompt::Question, 'convert date' do
4
+
5
+ subject(:prompt) { TTY::TestPrompt.new}
6
+
7
+ it 'fails to convert date' do
8
+ prompt.input << 'invalid'
9
+ prompt.input.rewind
10
+ expect {
11
+ prompt.ask("When were you born?", convert: :date)
12
+ }.to raise_error(TTY::Prompt::ConversionError)
13
+ end
14
+
4
15
  it 'converts date' do
5
- prompt = TTY::TestPrompt.new
6
16
  prompt.input << "20th April 1887"
7
17
  prompt.input.rewind
8
18
  response = prompt.ask("When were your born?", convert: :date)
@@ -13,7 +23,6 @@ RSpec.describe TTY::Prompt::Question, 'convert date' do
13
23
  end
14
24
 
15
25
  it "converts datetime" do
16
- prompt = TTY::TestPrompt.new
17
26
  prompt.input << "20th April 1887"
18
27
  prompt.input.rewind
19
28
  response = prompt.ask("When were your born?", convert: :datetime)
@@ -9,6 +9,6 @@ RSpec.describe TTY::Prompt::Question, 'convert file' do
9
9
  prompt.input.rewind
10
10
  answer = prompt.ask("Which file to open?", convert: :file)
11
11
  expect(answer).to eq(file)
12
- # expect(File).to have_received(:open).with(/test\.txt/)
12
+ expect(File).to have_received(:open).with(/test\.txt/)
13
13
  end
14
14
  end
@@ -1,8 +1,18 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  RSpec.describe TTY::Prompt::Question, 'convert numbers' do
4
+
5
+ subject(:prompt) { TTY::TestPrompt.new }
6
+
7
+ it 'fails to convert integer' do
8
+ prompt.input << 'invalid'
9
+ prompt.input.rewind
10
+ expect {
11
+ prompt.ask("What temparture?", convert: :int)
12
+ }.to raise_error(TTY::Prompt::ConversionError)
13
+ end
14
+
4
15
  it 'converts integer' do
5
- prompt = TTY::TestPrompt.new
6
16
  prompt.input << 35
7
17
  prompt.input.rewind
8
18
  answer = prompt.ask("What temperature?", convert: :int)
@@ -10,8 +20,15 @@ RSpec.describe TTY::Prompt::Question, 'convert numbers' do
10
20
  expect(answer).to eq(35)
11
21
  end
12
22
 
23
+ it 'fails to convert float' do
24
+ prompt.input << 'invalid'
25
+ prompt.input.rewind
26
+ expect {
27
+ prompt.ask("How tall are you?", convert: :float)
28
+ }.to raise_error(TTY::Prompt::ConversionError)
29
+ end
30
+
13
31
  it 'converts float' do
14
- prompt = TTY::TestPrompt.new
15
32
  number = 6.666
16
33
  prompt.input << number
17
34
  prompt.input.rewind
@@ -9,7 +9,7 @@ RSpec.describe TTY::Prompt::Question, 'convert path' do
9
9
  prompt.input << "/path/to/file"
10
10
  prompt.input.rewind
11
11
  answer = prompt.ask('File location?', convert: :path)
12
- # expect(Pathname).to have_received(:new).with(/path\/to\/file/)
13
12
  expect(answer).to eql(path)
13
+ expect(Pathname).to have_received(:new).with(/path\/to\/file/)
14
14
  end
15
15
  end
@@ -1,8 +1,10 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  RSpec.describe TTY::Prompt::Question, 'convert range' do
4
+
5
+ subject(:prompt) { TTY::TestPrompt.new}
6
+
4
7
  it 'converts with valid range' do
5
- prompt = TTY::TestPrompt.new
6
8
  prompt.input << "20-30"
7
9
  prompt.input.rewind
8
10
  answer = prompt.ask("Which age group?", convert: :range)
@@ -11,11 +13,10 @@ RSpec.describe TTY::Prompt::Question, 'convert range' do
11
13
  end
12
14
 
13
15
  it "fails to convert to range" do
14
- prompt = TTY::TestPrompt.new
15
16
  prompt.input << "abcd"
16
17
  prompt.input.rewind
17
18
  expect {
18
19
  prompt.ask('Which age group?', convert: :range)
19
- }.to raise_error(Necromancer::ConversionTypeError)
20
+ }.to raise_error(TTY::Prompt::ConversionError)
20
21
  end
21
22
  end
@@ -0,0 +1,93 @@
1
+ # encoding: utf-8
2
+
3
+ RSpec.describe TTY::Prompt do
4
+
5
+ subject(:prompt) { TTY::TestPrompt.new }
6
+
7
+ it "selects default option when return pressed immediately" do
8
+ choices = %w(/bin/nano /usr/bin/vim.basic /usr/bin/vim.tiny)
9
+ prompt.input << "\n"
10
+ prompt.input.rewind
11
+
12
+ expect(prompt.enum_select("Select an editor?", choices)).to eq('/bin/nano')
13
+ expect(prompt.output.string).to eq([
14
+ "Select an editor? \n",
15
+ "\e[32m 1) /bin/nano\e[0m\n",
16
+ " 2) /usr/bin/vim.basic\n",
17
+ " 3) /usr/bin/vim.tiny\n",
18
+ " Choose 1-3 [1]: ",
19
+ "\e[1000D\e[K\e[1A\e[1000D\e[K\e[1A\e[1000D\e[K\e[1A\e[1000D\e[K\e[1A\e[1000D\e[K\e[J",
20
+ "Select an editor? \e[32m/bin/nano\e[0m\n"
21
+ ].join)
22
+ end
23
+
24
+ it "selects option by index from the list" do
25
+ choices = %w(/bin/nano /usr/bin/vim.basic /usr/bin/vim.tiny)
26
+ prompt.input << "3\n"
27
+ prompt.input.rewind
28
+
29
+ expect(prompt.enum_select("Select an editor?", choices, default: 2)).to eq('/usr/bin/vim.tiny')
30
+ expect(prompt.output.string).to eq([
31
+ "Select an editor? \n",
32
+ " 1) /bin/nano\n",
33
+ "\e[32m 2) /usr/bin/vim.basic\e[0m\n",
34
+ " 3) /usr/bin/vim.tiny\n",
35
+ " Choose 1-3 [2]: ",
36
+ "\e[1000D\e[K\e[1A\e[1000D\e[K\e[1A\e[1000D\e[K\e[1A\e[1000D\e[K\e[1A\e[1000D\e[K\e[J",
37
+ "Select an editor? \n",
38
+ " 1) /bin/nano\n",
39
+ " 2) /usr/bin/vim.basic\n",
40
+ "\e[32m 3) /usr/bin/vim.tiny\e[0m\n",
41
+ " Choose 1-3 [2]: 3",
42
+ "\e[1000D\e[K\e[1A\e[1000D\e[K\e[1A\e[1000D\e[K\e[1A\e[1000D\e[K\e[1A\e[1000D\e[K\e[J",
43
+ "Select an editor? \e[32m/usr/bin/vim.tiny\e[0m\n"
44
+ ].join)
45
+ end
46
+
47
+ it "selects option through DSL" do
48
+ prompt.input << "\n"
49
+ prompt.input.rewind
50
+ value = prompt.enum_select("Select an editor?") do |menu|
51
+ menu.default 2
52
+ menu.enum '.'
53
+
54
+ menu.choice "/bin/nano"
55
+ menu.choice "/usr/bin/vim.basic"
56
+ menu.choice "/usr/bin/vim.tiny"
57
+ end
58
+
59
+ expect(value).to eq('/usr/bin/vim.basic')
60
+ expect(prompt.output.string).to eq([
61
+ "Select an editor? \n",
62
+ " 1. /bin/nano\n",
63
+ "\e[32m 2. /usr/bin/vim.basic\e[0m\n",
64
+ " 3. /usr/bin/vim.tiny\n",
65
+ " Choose 1-3 [2]: ",
66
+ "\e[1000D\e[K\e[1A\e[1000D\e[K\e[1A\e[1000D\e[K\e[1A\e[1000D\e[K\e[1A\e[1000D\e[K\e[J",
67
+ "Select an editor? \e[32m/usr/bin/vim.basic\e[0m\n"
68
+ ].join)
69
+ end
70
+
71
+ it "selects option through DSL with key and value" do
72
+ prompt.input << "\n"
73
+ prompt.input.rewind
74
+ value = prompt.enum_select("Select an editor?") do |menu|
75
+ menu.default 2
76
+
77
+ menu.choice :nano, '/bin/nano'
78
+ menu.choice :vim, '/usr/bin/vim'
79
+ menu.choice :emacs, '/usr/bin/emacs'
80
+ end
81
+
82
+ expect(value).to eq('/usr/bin/vim')
83
+ expect(prompt.output.string).to eq([
84
+ "Select an editor? \n",
85
+ " 1) nano\n",
86
+ "\e[32m 2) vim\e[0m\n",
87
+ " 3) emacs\n",
88
+ " Choose 1-3 [2]: ",
89
+ "\e[1000D\e[K\e[1A\e[1000D\e[K\e[1A\e[1000D\e[K\e[1A\e[1000D\e[K\e[1A\e[1000D\e[K\e[J",
90
+ "Select an editor? \e[32mvim\e[0m\n"
91
+ ].join)
92
+ end
93
+ end
@@ -74,6 +74,8 @@ RSpec.describe TTY::Prompt do
74
74
  prompt.input << " \r"
75
75
  prompt.input.rewind
76
76
  value = prompt.multi_select("Select drinks?") do |menu|
77
+ menu.enum ')'
78
+
77
79
  menu.choice :vodka, {score: 1}
78
80
  menu.choice :beer, 2
79
81
  menu.choice :wine, 3
@@ -81,19 +83,19 @@ RSpec.describe TTY::Prompt do
81
83
  end
82
84
  expect(value).to eq([{score: 1}])
83
85
  expect(prompt.output.string).to eq([
84
- "\e[?25lSelect drinks? \e[90m(Use arrow keys, press Space to select and Enter to finish)\e[0m\n",
85
- "‣ ⬡ vodka\n",
86
- " ⬡ beer\n",
87
- " ⬡ wine\n",
88
- " ⬡ whisky\n",
89
- " ⬡ bourbon",
86
+ "\e[?25lSelect drinks? \e[90m(Use arrow or number (0-9) keys, press Space to select and Enter to finish)\e[0m\n",
87
+ "‣ ⬡ 1) vodka\n",
88
+ " ⬡ 2) beer\n",
89
+ " ⬡ 3) wine\n",
90
+ " ⬡ 4) whisky\n",
91
+ " ⬡ 5) bourbon",
90
92
  "\e[1000D\e[K\e[1A" * 5, "\e[1000D\e[K",
91
93
  "Select drinks? vodka\n",
92
- "‣ \e[32m⬢\e[0m vodka\n",
93
- " ⬡ beer\n",
94
- " ⬡ wine\n",
95
- " ⬡ whisky\n",
96
- " ⬡ bourbon",
94
+ "‣ \e[32m⬢\e[0m 1) vodka\n",
95
+ " ⬡ 2) beer\n",
96
+ " ⬡ 3) wine\n",
97
+ " ⬡ 4) whisky\n",
98
+ " ⬡ 5) bourbon",
97
99
  "\e[1000D\e[K\e[1A" * 5, "\e[1000D\e[K",
98
100
  "Select drinks? \e[32mvodka\e[0m\n\e[?25h"
99
101
  ].join)
@@ -151,7 +153,7 @@ RSpec.describe TTY::Prompt do
151
153
  menu.choice :whisky, {score: 40}
152
154
  menu.choice :bourbon, {score: 50}
153
155
  end
154
- }.to raise_error(TTY::PromptConfigurationError,
156
+ }.to raise_error(TTY::Prompt::ConfigurationError,
155
157
  /default index `6` out of range \(1 - 5\)/)
156
158
  end
157
159
 
@@ -0,0 +1,97 @@
1
+ # encoding: utf-8
2
+
3
+ RSpec.describe TTY::Prompt::Question do
4
+
5
+ subject(:prompt) { TTY::TestPrompt.new }
6
+
7
+ it "passes range check" do
8
+ question = described_class.new(prompt)
9
+ question.in 1..10
10
+
11
+ result = TTY::Prompt::Question::Checks::CheckRange.call(question, 2)
12
+
13
+ expect(result).to eq([2])
14
+ end
15
+
16
+ it "fails range check" do
17
+ question = described_class.new(prompt, messages: TTY::Prompt.messages)
18
+ question.in 1..10
19
+
20
+ result = TTY::Prompt::Question::Checks::CheckRange.call(question, 11)
21
+
22
+ expect(result).to eq([11, ["Value 11 must be within the range 1..10"]])
23
+ end
24
+
25
+ it "fails range check" do
26
+ question = described_class.new(prompt)
27
+ question.in 1..10, 'Outside of range!'
28
+
29
+ result = TTY::Prompt::Question::Checks::CheckRange.call(question, 11)
30
+
31
+ expect(result).to eq([11, ['Outside of range!']])
32
+ end
33
+
34
+ it "passes validation check" do
35
+ question = described_class.new(prompt)
36
+ question.validate(/\A\d{5}\Z/)
37
+
38
+ result = TTY::Prompt::Question::Checks::CheckValidation.call(question, '12345')
39
+
40
+ expect(result).to eq(['12345'])
41
+ end
42
+
43
+ it "fails validation check" do
44
+ question = described_class.new(prompt, messages: TTY::Prompt.messages)
45
+ question.validate(/\A\d{5}\Z/)
46
+
47
+ result = TTY::Prompt::Question::Checks::CheckValidation.call(question, '123')
48
+
49
+ expect(result).to eq(['123', ['Your answer is invalid (must match /\\A\\d{5}\\Z/)']])
50
+ end
51
+
52
+ it "fails validation check with inlined custom message" do
53
+ question = described_class.new(prompt)
54
+ question.validate(/\A\w+@\w+\.\w+\Z/, 'Invalid email address')
55
+
56
+ result = TTY::Prompt::Question::Checks::CheckValidation.call(question, 'piotr@com')
57
+
58
+ expect(result).to eq(['piotr@com', ['Invalid email address']])
59
+ end
60
+
61
+ it "fails validation check with custom message" do
62
+ question = described_class.new(prompt)
63
+ question.validate(/\A\w+@\w+\.\w+\Z/)
64
+ question.messages[:valid?] = 'Invalid email address'
65
+
66
+ result = TTY::Prompt::Question::Checks::CheckValidation.call(question, 'piotr@com')
67
+
68
+ expect(result).to eq(['piotr@com', ['Invalid email address']])
69
+ end
70
+
71
+ it "passes required check" do
72
+ question = described_class.new(prompt)
73
+ question.required true
74
+
75
+ result = TTY::Prompt::Question::Checks::CheckRequired.call(question, 'Piotr')
76
+
77
+ expect(result).to eq(['Piotr'])
78
+ end
79
+
80
+ it "fails required check" do
81
+ question = described_class.new(prompt, messages: TTY::Prompt.messages)
82
+ question.required true
83
+
84
+ result = TTY::Prompt::Question::Checks::CheckRequired.call(question, nil)
85
+
86
+ expect(result).to eq([nil, ['Value must be provided']])
87
+ end
88
+
89
+ it "fails required check with custom message" do
90
+ question = described_class.new(prompt)
91
+ question.required true, 'Required input'
92
+
93
+ result = TTY::Prompt::Question::Checks::CheckRequired.call(question, nil)
94
+
95
+ expect(result).to eq([nil, ['Required input']])
96
+ end
97
+ end
@@ -0,0 +1,67 @@
1
+ # encoding
2
+
3
+ RSpec.describe TTY::Prompt::Reader::KeyEvent, '::from' do
4
+
5
+ it "parses ctrl+h" do
6
+ event = described_class.from("\b")
7
+ expect(event.key.name).to eq(:backspace)
8
+ expect(event.value).to eq("\b")
9
+ end
10
+
11
+ it "parses lowercase char" do
12
+ event = described_class.from('a')
13
+ expect(event.key.name).to eq('a')
14
+ expect(event.value).to eq('a')
15
+ end
16
+
17
+ it "parses uppercase char" do
18
+ event = described_class.from('A')
19
+ expect(event.key.name).to eq('a')
20
+ expect(event.value).to eq('A')
21
+ end
22
+
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)
56
+ end
57
+
58
+ it "parses end key" do
59
+ event = described_class.from("\e[F")
60
+ expect(event.key.name).to eq(:end)
61
+ end
62
+
63
+ it "parses home key" do
64
+ event = described_class.from("\e[H")
65
+ expect(event.key.name).to eq(:home)
66
+ end
67
+ end