tty-prompt 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +9 -6
  3. data/CHANGELOG.md +40 -3
  4. data/Gemfile +0 -1
  5. data/README.md +246 -65
  6. data/examples/ask.rb +7 -0
  7. data/examples/echo.rb +7 -0
  8. data/examples/in.rb +7 -0
  9. data/examples/mask.rb +9 -0
  10. data/examples/multi_select.rb +8 -0
  11. data/examples/select.rb +8 -0
  12. data/examples/validation.rb +9 -0
  13. data/examples/yes_no.rb +7 -0
  14. data/lib/tty-prompt.rb +6 -4
  15. data/lib/tty/prompt.rb +100 -25
  16. data/lib/tty/prompt/choice.rb +1 -1
  17. data/lib/tty/prompt/converter_dsl.rb +19 -0
  18. data/lib/tty/prompt/converter_registry.rb +56 -0
  19. data/lib/tty/prompt/converters.rb +77 -0
  20. data/lib/tty/prompt/evaluator.rb +29 -0
  21. data/lib/tty/prompt/list.rb +38 -36
  22. data/lib/tty/prompt/mask_question.rb +85 -0
  23. data/lib/tty/prompt/multi_list.rb +21 -32
  24. data/lib/tty/prompt/question.rb +184 -162
  25. data/lib/tty/prompt/question/checks.rb +85 -0
  26. data/lib/tty/prompt/question/modifier.rb +4 -5
  27. data/lib/tty/prompt/question/validation.rb +29 -35
  28. data/lib/tty/prompt/reader.rb +98 -52
  29. data/lib/tty/prompt/reader/codes.rb +63 -0
  30. data/lib/tty/prompt/reader/key_event.rb +67 -0
  31. data/lib/tty/prompt/reader/mode.rb +66 -0
  32. data/lib/tty/prompt/reader/mode/echo.rb +43 -0
  33. data/lib/tty/prompt/reader/mode/raw.rb +43 -0
  34. data/lib/tty/prompt/result.rb +42 -0
  35. data/lib/tty/prompt/statement.rb +9 -14
  36. data/lib/tty/prompt/suggestion.rb +4 -2
  37. data/lib/tty/prompt/symbols.rb +13 -0
  38. data/lib/tty/prompt/test.rb +3 -2
  39. data/lib/tty/prompt/utils.rb +1 -1
  40. data/lib/tty/prompt/version.rb +1 -1
  41. data/spec/unit/ask_spec.rb +31 -48
  42. data/spec/unit/choice/eql_spec.rb +0 -2
  43. data/spec/unit/choice/from_spec.rb +0 -2
  44. data/spec/unit/choices/add_spec.rb +0 -2
  45. data/spec/unit/choices/each_spec.rb +0 -2
  46. data/spec/unit/choices/new_spec.rb +0 -2
  47. data/spec/unit/choices/pluck_spec.rb +0 -2
  48. data/spec/unit/converters/convert_bool_spec.rb +58 -0
  49. data/spec/unit/{response/read_char_spec.rb → converters/convert_char_spec.rb} +2 -4
  50. data/spec/unit/converters/convert_custom_spec.rb +14 -0
  51. data/spec/unit/converters/convert_date_spec.rb +25 -0
  52. data/spec/unit/converters/convert_file_spec.rb +14 -0
  53. data/spec/unit/{response/read_number_spec.rb → converters/convert_number_spec.rb} +5 -7
  54. data/spec/unit/converters/convert_path_spec.rb +15 -0
  55. data/spec/unit/{response/read_range_spec.rb → converters/convert_range_spec.rb} +3 -5
  56. data/spec/unit/converters/convert_regex_spec.rb +12 -0
  57. data/spec/unit/converters/convert_string_spec.rb +21 -0
  58. data/spec/unit/distance/distance_spec.rb +0 -2
  59. data/spec/unit/error_spec.rb +0 -6
  60. data/spec/unit/evaluator_spec.rb +67 -0
  61. data/spec/unit/keypress_spec.rb +19 -0
  62. data/spec/unit/mask_spec.rb +95 -0
  63. data/spec/unit/multi_select_spec.rb +36 -24
  64. data/spec/unit/multiline_spec.rb +19 -0
  65. data/spec/unit/new_spec.rb +18 -0
  66. data/spec/unit/ok_spec.rb +10 -0
  67. data/spec/unit/question/default_spec.rb +17 -4
  68. data/spec/unit/question/echo_spec.rb +31 -0
  69. data/spec/unit/question/in_spec.rb +48 -16
  70. data/spec/unit/question/initialize_spec.rb +2 -9
  71. data/spec/unit/question/modifier/apply_to_spec.rb +9 -16
  72. data/spec/unit/question/modifier/letter_case_spec.rb +0 -2
  73. data/spec/unit/question/modifier/whitespace_spec.rb +12 -20
  74. data/spec/unit/question/modify_spec.rb +3 -7
  75. data/spec/unit/question/required_spec.rb +20 -14
  76. data/spec/unit/question/validate_spec.rb +20 -19
  77. data/spec/unit/question/validation/call_spec.rb +15 -6
  78. data/spec/unit/question/validation/coerce_spec.rb +17 -11
  79. data/spec/unit/reader/publish_keypress_event_spec.rb +81 -0
  80. data/spec/unit/reader/read_keypress_spec.rb +22 -0
  81. data/spec/unit/reader/read_line_spec.rb +31 -0
  82. data/spec/unit/reader/read_multiline_spec.rb +37 -0
  83. data/spec/unit/result_spec.rb +40 -0
  84. data/spec/unit/say_spec.rb +18 -23
  85. data/spec/unit/select_spec.rb +37 -32
  86. data/spec/unit/statement/initialize_spec.rb +4 -4
  87. data/spec/unit/suggest_spec.rb +0 -2
  88. data/spec/unit/warn_spec.rb +0 -5
  89. data/spec/unit/yes_no_spec.rb +70 -0
  90. data/tty-prompt.gemspec +7 -4
  91. metadata +123 -40
  92. data/lib/tty/prompt/codes.rb +0 -32
  93. data/lib/tty/prompt/cursor.rb +0 -131
  94. data/lib/tty/prompt/error.rb +0 -26
  95. data/lib/tty/prompt/mode.rb +0 -64
  96. data/lib/tty/prompt/mode/echo.rb +0 -41
  97. data/lib/tty/prompt/mode/raw.rb +0 -41
  98. data/lib/tty/prompt/response.rb +0 -247
  99. data/lib/tty/prompt/response_delegation.rb +0 -42
  100. data/spec/unit/cursor/new_spec.rb +0 -74
  101. data/spec/unit/question/character_spec.rb +0 -13
  102. data/spec/unit/reader/getc_spec.rb +0 -42
  103. data/spec/unit/response/read_bool_spec.rb +0 -58
  104. data/spec/unit/response/read_date_spec.rb +0 -16
  105. data/spec/unit/response/read_email_spec.rb +0 -45
  106. data/spec/unit/response/read_multiple_spec.rb +0 -21
  107. data/spec/unit/response/read_spec.rb +0 -69
  108. data/spec/unit/response/read_string_spec.rb +0 -14
@@ -1,10 +1,10 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'spec_helper'
4
-
5
3
  RSpec.describe TTY::Prompt::Question, '#modify' do
4
+
5
+ subject(:prompt) { TTY::TestPrompt.new }
6
+
6
7
  it 'preserves answer for unkown modification' do
7
- prompt = TTY::TestPrompt.new
8
8
  prompt.input << 'piotr'
9
9
  prompt.input.rewind
10
10
  answer = prompt.ask("What is your name?") { |q| q.modify(:none) }
@@ -12,7 +12,6 @@ RSpec.describe TTY::Prompt::Question, '#modify' do
12
12
  end
13
13
 
14
14
  it 'converts to upper case' do
15
- prompt = TTY::TestPrompt.new
16
15
  prompt.input << 'piotr'
17
16
  prompt.input.rewind
18
17
  answer = prompt.ask("What is your name?") { |q| q.modify(:upcase) }
@@ -20,7 +19,6 @@ RSpec.describe TTY::Prompt::Question, '#modify' do
20
19
  end
21
20
 
22
21
  it 'trims whitespace' do
23
- prompt = TTY::TestPrompt.new
24
22
  prompt.input << " Some white\t space\t \there! \n"
25
23
  prompt.input.rewind
26
24
  answer = prompt.ask('Enter some text: ') { |q| q.modify(:trim) }
@@ -28,7 +26,6 @@ RSpec.describe TTY::Prompt::Question, '#modify' do
28
26
  end
29
27
 
30
28
  it 'collapses whitespace' do
31
- prompt = TTY::TestPrompt.new
32
29
  prompt.input << " Some white\t space\t \there! \n"
33
30
  prompt.input.rewind
34
31
  answer = prompt.ask('Enter some text: ') { |q| q.modify(:collapse) }
@@ -36,7 +33,6 @@ RSpec.describe TTY::Prompt::Question, '#modify' do
36
33
  end
37
34
 
38
35
  it 'strips and collapses whitespace' do
39
- prompt = TTY::TestPrompt.new
40
36
  prompt.input << " Some white\t space\t \there! \n"
41
37
  prompt.input.rewind
42
38
  answer = prompt.ask('Enter some text: ') { |q| q.modify(:strip, :collapse) }
@@ -1,28 +1,34 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'spec_helper'
4
-
5
3
  RSpec.describe TTY::Prompt::Question, '#required' do
6
- it 'requires value to be present with helper' do
7
- prompt = TTY::TestPrompt.new
8
- prompt.input << ''
4
+
5
+ subject(:prompt) { TTY::TestPrompt.new }
6
+
7
+ it 'requires value to be present' do
8
+ prompt.input << "Piotr"
9
9
  prompt.input.rewind
10
- expect {
11
- prompt.ask('What is your name?') { |q| q.required(true) }
12
- }.to raise_error(ArgumentError)
10
+ prompt.ask('What is your name?') { |q| q.required(true) }
11
+ expect(prompt.output.string).to eq([
12
+ "What is your name? ",
13
+ "\e[1000D\e[K\e[1A",
14
+ "\e[1000D\e[K",
15
+ "What is your name? \e[32mPiotr\e[0m\n"
16
+ ].join)
13
17
  end
14
18
 
15
19
  it 'requires value to be present with option' do
16
- prompt = TTY::TestPrompt.new
17
- prompt.input << ''
20
+ prompt.input << "Piotr"
18
21
  prompt.input.rewind
19
- expect {
20
- prompt.ask('What is your name?', required: true)
21
- }.to raise_error(ArgumentError)
22
+ prompt.ask('What is your name?') { |q| q.required(true) }
23
+ expect(prompt.output.string).to eq([
24
+ "What is your name? ",
25
+ "\e[1000D\e[K\e[1A",
26
+ "\e[1000D\e[K",
27
+ "What is your name? \e[32mPiotr\e[0m\n"
28
+ ].join)
22
29
  end
23
30
 
24
31
  it "doesn't require value to be present" do
25
- prompt = TTY::TestPrompt.new
26
32
  prompt.input << ''
27
33
  prompt.input.rewind
28
34
  answer = prompt.ask('What is your name?') { |q| q.required(false) }
@@ -1,38 +1,39 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'spec_helper'
4
-
5
3
  RSpec.describe TTY::Prompt::Question, '#validate' do
6
- it 'fails to validate input' do
7
- prompt = TTY::TestPrompt.new
8
- prompt.input << 'piotrmurach'
9
- prompt.input.rewind
10
- expect {
11
- prompt.ask('What is your username?') { |q|
12
- q.validate(/^[^\.]+\.[^\.]+/)
13
- }
14
- }.to raise_error(ArgumentError)
15
- end
4
+
5
+ subject(:prompt) { TTY::TestPrompt.new }
16
6
 
17
7
  it 'validates input with regex' do
18
- prompt = TTY::TestPrompt.new
19
8
  prompt.input << 'piotr.murach'
20
9
  prompt.input.rewind
21
- answer = prompt.ask('What is your username?') { |q|
10
+ answer = prompt.ask('What is your username?') do |q|
22
11
  q.validate(/^[^\.]+\.[^\.]+/)
23
- }
12
+ end
24
13
  expect(answer).to eq('piotr.murach')
14
+ expect(prompt.output.string).to eq([
15
+ "What is your username? ",
16
+ "\e[1000D\e[K\e[1A",
17
+ "\e[1000D\e[K",
18
+ "What is your username? \e[32mpiotr.murach\e[0m\n"
19
+ ].join)
25
20
  end
26
21
 
27
22
  it 'validates input with proc' do
28
- prompt = TTY::TestPrompt.new
29
23
  prompt.input << 'piotr.murach'
30
24
  prompt.input.rewind
31
- answer = prompt.ask('What is your username?') { |q|
25
+ answer = prompt.ask('What is your username?') do |q|
32
26
  q.validate { |input| input =~ /^[^\.]+\.[^\.]+/ }
33
- }
27
+ end
34
28
  expect(answer).to eq('piotr.murach')
35
29
  end
36
30
 
37
- it 'understands custom validation like :email'
31
+ it 'understands custom validation like :email' do
32
+ prompt.input << 'piotr@example.com'
33
+ prompt.input.rewind
34
+ answer = prompt.ask('What is your email?') do |q|
35
+ q.validate :email
36
+ end
37
+ expect(answer).to eq('piotr@example.com')
38
+ end
38
39
  end
@@ -1,22 +1,31 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'spec_helper'
4
-
5
3
  RSpec.describe TTY::Prompt::Question::Validation, '#call' do
6
4
  let(:pattern) { /^[^\.]+\.[^\.]+/ }
7
- let(:validation) { described_class.new(pattern) }
8
5
 
9
6
  it "validates nil input" do
7
+ validation = described_class.new(pattern)
10
8
  expect(validation.(nil)).to eq(false)
11
9
  end
12
10
 
13
11
  it "validates successfully when the value matches pattern" do
12
+ validation = described_class.new(pattern)
14
13
  expect(validation.('piotr.murach')).to eq(true)
15
14
  end
16
15
 
16
+ it "validates with a proc" do
17
+ pat = proc { |input| !pattern.match(input).nil? }
18
+ validation = described_class.new(pat)
19
+ expect(validation.call('piotr.murach')).to eq(true)
20
+ end
21
+
22
+ it "validates with custom name" do
23
+ validation = described_class.new(:email)
24
+ expect(validation.call('piotr@example.com')).to eq(true)
25
+ end
26
+
17
27
  it "fails validation when not maching pattern" do
18
- expect {
19
- validation.('piotrmurach')
20
- }.to raise_error(TTY::Prompt::InvalidArgument)
28
+ validation = described_class.new(pattern)
29
+ expect(validation.('piotrmurach')).to eq(false)
21
30
  end
22
31
  end
@@ -1,24 +1,30 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'spec_helper'
4
-
5
3
  RSpec.describe TTY::Prompt::Question::Validation, '#coerce' do
6
4
  let(:instance) { described_class.new }
7
5
 
8
- it "coerces into proc" do
9
- validation = lambda { "^[^\.]+\.[^\.]+" }
10
- expect(instance.coerce(validation)).to be_kind_of(Proc)
6
+ it "coerces lambda into proc" do
7
+ pattern = lambda { "^[^\.]+\.[^\.]+" }
8
+ validation = described_class.new(pattern)
9
+ expect(validation.pattern).to be_a(Proc)
10
+ end
11
+
12
+ it "doesn't coerce symbols" do
13
+ pattern = :email
14
+ validation =described_class.new(pattern)
15
+ expect(validation.pattern).to eq(:email)
11
16
  end
12
17
 
13
- it "cources into regex" do
14
- validation = "^[^\.]+\.[^\.]+"
15
- expect(instance.coerce(validation)).to be_kind_of(Regexp)
18
+ it "coerces into regex" do
19
+ pattern = /^[^\.]+\.[^\.]+/
20
+ validation = described_class.new(pattern)
21
+ expect(validation.pattern).to be_a(Regexp)
16
22
  end
17
23
 
18
- it "fails to coerce validation" do
19
- validation = Object.new
24
+ it "fails to coerce pattern into validation" do
25
+ pattern = Object.new
20
26
  expect {
21
- instance.coerce(validation)
27
+ described_class.new(pattern)
22
28
  }.to raise_error(TTY::Prompt::ValidationCoercion)
23
29
  end
24
30
  end
@@ -0,0 +1,81 @@
1
+ # encoding: utf-8
2
+
3
+ RSpec.describe TTY::Prompt::Reader, '#publish_keypress_event' do
4
+ let(:input) { StringIO.new }
5
+ let(:out) { StringIO.new }
6
+ let(:reader) { described_class.new(input, out) }
7
+
8
+ it "publishes :keypress events" do
9
+ input << "abc\n"
10
+ input.rewind
11
+ chars = []
12
+ reader.on(:keypress) { |event| chars << event.value }
13
+ answer = reader.read_line
14
+ expect(chars).to eq(%w(a b c))
15
+ expect(answer).to eq("abc")
16
+ end
17
+
18
+ it "publishes :keyup for read_keypress" do
19
+ input << "\e[Aaa"
20
+ input.rewind
21
+ keys = []
22
+ reader.on(:keypress) { |event| keys << "keypress_#{event.value}" }
23
+ reader.on(:keyup) { |event| keys << "keyup_#{event.value}" }
24
+ reader.on(:keydown) { |event| keys << "keydown_#{event.value}" }
25
+
26
+ answer = reader.read_keypress
27
+ expect(keys).to eq(["keyup_\e[A", "keypress_\e[A"])
28
+ expect(answer).to eq("\e[A")
29
+ end
30
+
31
+ it "publishes :keydown event for read_keypress" do
32
+ input << "\e[Baa"
33
+ input.rewind
34
+ keys = []
35
+ reader.on(:keypress) { |event| keys << "keypress_#{event.value}" }
36
+ reader.on(:keyup) { |event| keys << "keyup_#{event.value}" }
37
+ reader.on(:keydown) { |event| keys << "keydown_#{event.value}" }
38
+
39
+ answer = reader.read_keypress
40
+ expect(keys).to eq(["keydown_\e[B", "keypress_\e[B"])
41
+ expect(answer).to eq("\e[B")
42
+ end
43
+
44
+ it "publishes :keynum event" do
45
+ input << "5aa"
46
+ input.rewind
47
+ keys = []
48
+ reader.on(:keypress) { |event| keys << "keypress_#{event.value}" }
49
+ reader.on(:keyup) { |event| keys << "keyup_#{event.value}" }
50
+ reader.on(:keynum) { |event| keys << "keynum_#{event.value}" }
51
+
52
+ answer = reader.read_keypress
53
+ expect(keys).to eq(["keynum_5", "keypress_5"])
54
+ expect(answer).to eq("5")
55
+ end
56
+
57
+ it "publishes :keyreturn event" do
58
+ input << "\r"
59
+ input.rewind
60
+ keys = []
61
+ reader.on(:keypress) { |event| keys << "keypress" }
62
+ reader.on(:keyup) { |event| keys << "keyup" }
63
+ reader.on(:keyreturn) { |event| keys << "keyreturn" }
64
+
65
+ answer = reader.read_keypress
66
+ expect(keys).to eq(["keyreturn", "keypress"])
67
+ expect(answer).to eq("\r")
68
+ end
69
+
70
+ it "subscribes to multiple events" do
71
+ input << "\n"
72
+ input.rewind
73
+ keys = []
74
+ reader.on(:keyenter) { |event| keys << "keyenter" }
75
+ .on(:keypress) { |event| keys << "keypress" }
76
+
77
+ answer = reader.read_keypress
78
+ expect(keys).to eq(["keyenter", "keypress"])
79
+ expect(answer).to eq("\n")
80
+ end
81
+ end
@@ -0,0 +1,22 @@
1
+ # encoding: utf-8
2
+
3
+ RSpec.describe TTY::Prompt::Reader, '#read_keypress' do
4
+ let(:input) { StringIO.new }
5
+ let(:out) { StringIO.new }
6
+ let(:reader) { described_class.new(input, out) }
7
+
8
+ it "reads single key press" do
9
+ input << "\e[Aaaaaaa\n"
10
+ input.rewind
11
+ answer = reader.read_keypress
12
+ expect(answer).to eq("\e[A")
13
+ end
14
+
15
+ it "stops reading when ctrl-c pressed" do
16
+ input << "\x03"
17
+ input.rewind
18
+ expect {
19
+ reader.read_keypress
20
+ }.to raise_error(SystemExit)
21
+ end
22
+ end
@@ -0,0 +1,31 @@
1
+ # encoding: utf-8
2
+
3
+ RSpec.describe TTY::Prompt::Reader, '#read_line' do
4
+ let(:input) { StringIO.new }
5
+ let(:output) { StringIO.new }
6
+
7
+ subject(:reader) { described_class.new(input, output) }
8
+
9
+ it 'masks characters' do
10
+ mask = '*'
11
+ input << "password\n"
12
+ input.rewind
13
+ answer = reader.read_line(mask)
14
+ expect(answer).to eq("password")
15
+ end
16
+
17
+ it "echoes characters back" do
18
+ input << "password\n"
19
+ input.rewind
20
+ answer = reader.read_line
21
+ expect(answer).to eq("password")
22
+ expect(output.string).to eq("")
23
+ end
24
+
25
+ it 'deletes characters when backspace pressed' do
26
+ input << "aa\ba\bcc"
27
+ input.rewind
28
+ answer = reader.read_line
29
+ expect(answer).to eq('acc')
30
+ end
31
+ end
@@ -0,0 +1,37 @@
1
+ # encoding: utf-8
2
+
3
+ RSpec.describe TTY::Prompt::Reader, '#read_multiline' do
4
+ let(:input) { StringIO.new }
5
+ let(:output) { StringIO.new }
6
+
7
+ subject(:reader) { described_class.new(input, output) }
8
+
9
+ it 'reads no lines' do
10
+ input << ''
11
+ input.rewind
12
+ answer = reader.read_multiline
13
+ expect(answer).to eq([])
14
+ end
15
+
16
+ it "reads a line" do
17
+ input << "Single line"
18
+ input.rewind
19
+ answer = reader.read_multiline
20
+ expect(answer).to eq(['Single line'])
21
+ end
22
+
23
+ it 'reads few lines' do
24
+ input << "First line\nSecond line\nThird line"
25
+ input.rewind
26
+ answer = reader.read_multiline
27
+ expect(answer).to eq(['First line', 'Second line', 'Third line'])
28
+ end
29
+
30
+ it 'reads and yiels every line' do
31
+ input << "First line\nSecond line\nThird line"
32
+ input.rewind
33
+ lines = []
34
+ reader.read_multiline { |line| lines << line }
35
+ expect(lines).to eq(['First line', 'Second line', 'Third line'])
36
+ end
37
+ end
@@ -0,0 +1,40 @@
1
+ # encoding: utf-8
2
+
3
+ RSpec.describe TTY::Prompt::Result do
4
+ it "checks value to be invalid" do
5
+ question = double(:question)
6
+ result = TTY::Prompt::Result.new(question, nil)
7
+
8
+ answer = result.with { |quest, value|
9
+ if value.nil?
10
+ [value, ["`#{value}` provided cannot be empty"]]
11
+ else
12
+ value
13
+ end
14
+ }
15
+ expect(answer).to be_a(TTY::Prompt::Result::Failure)
16
+ expect(answer.success?).to eq(false)
17
+ expect(answer.errors).to eq(["`` provided cannot be empty"])
18
+ end
19
+
20
+ it "checks value to be valid" do
21
+ question = double(:question)
22
+ result = TTY::Prompt::Result.new(question, 'Piotr')
23
+
24
+ CheckRequired = Class.new do
25
+ def self.call(quest, value)
26
+ if value.nil?
27
+ [value, ["`#{value}` provided cannot be empty"]]
28
+ else
29
+ value
30
+ end
31
+ end
32
+ end
33
+
34
+ answer = result.with(CheckRequired)
35
+ expect(answer).to be_a(TTY::Prompt::Result::Success)
36
+ expect(answer.success?).to eq(true)
37
+ expect(answer.value).to eq('Piotr')
38
+ expect(answer.errors).to eq([])
39
+ end
40
+ end