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
@@ -0,0 +1,85 @@
1
+ # encoding: utf-8
2
+
3
+ module TTY
4
+ class Prompt
5
+ class Question
6
+ module Checks
7
+ # Check if modifications are applicable
8
+ class CheckModifier
9
+ def self.call(question, value)
10
+ if !question.modifier.nil? || question.modifier
11
+ [Modifier.new(question.modifier).apply_to(value)]
12
+ else
13
+ [value]
14
+ end
15
+ end
16
+ end
17
+
18
+ # Check if value is within range
19
+ class CheckRange
20
+ def self.float?(value)
21
+ !/[-+]?(\d*[.])?\d+/.match(value.to_s).nil?
22
+ end
23
+
24
+ def self.int?(value)
25
+ !/^[-+]?\d+$/.match(value.to_s).nil?
26
+ end
27
+
28
+ def self.cast(value)
29
+ if float?(value)
30
+ value.to_f
31
+ elsif int?(value)
32
+ value.to_i
33
+ else
34
+ value
35
+ end
36
+ end
37
+
38
+ def self.call(question, value)
39
+ if !question.in? ||
40
+ (question.in? && question.in.include?(cast(value)))
41
+ [value]
42
+ else
43
+ [value, ["Value #{value} is not included in the range #{question.in}"]]
44
+ end
45
+ end
46
+ end
47
+
48
+ # Check if input requires validation
49
+ class CheckValidation
50
+ def self.call(question, value)
51
+ if !question.validation? ||
52
+ (question.validation? &&
53
+ Validation.new(question.validation).call(value))
54
+ [value]
55
+ else
56
+ [value, ["Your answer is invalid (must match #{question.validation.inspect})"]]
57
+ end
58
+ end
59
+ end
60
+
61
+ # Check if default value provided
62
+ class CheckDefault
63
+ def self.call(question, value)
64
+ if value.nil? && question.default?
65
+ [question.default]
66
+ else
67
+ [value]
68
+ end
69
+ end
70
+ end
71
+
72
+ # Check if input is required
73
+ class CheckRequired
74
+ def self.call(question, value)
75
+ if question.required? && !question.default? && value.nil?
76
+ [value, ['No value provided for required']]
77
+ else
78
+ [value]
79
+ end
80
+ end
81
+ end
82
+ end # Checks
83
+ end # Question
84
+ end # Prompt
85
+ end # TTY
@@ -6,13 +6,12 @@ module TTY
6
6
  # A class representing String modifications.
7
7
  class Modifier
8
8
  attr_reader :modifiers
9
- private :modifiers
10
9
 
11
10
  # Initialize a Modifier
12
11
  #
13
12
  # @api public
14
- def initialize(*modifiers)
15
- @modifiers = Array(modifiers)
13
+ def initialize(modifiers)
14
+ @modifiers = modifiers
16
15
  end
17
16
 
18
17
  # Change supplied value according to the given string transformation.
@@ -26,8 +25,8 @@ module TTY
26
25
  # @api private
27
26
  def apply_to(value)
28
27
  modifiers.reduce(value) do |result, mod|
29
- result = Modifier.letter_case mod, result
30
- Modifier.whitespace mod, result
28
+ result = Modifier.letter_case(mod, result)
29
+ Modifier.whitespace(mod, result)
31
30
  end
32
31
  end
33
32
 
@@ -5,53 +5,47 @@ module TTY
5
5
  class Question
6
6
  # A class representing question validation.
7
7
  class Validation
8
- # @api private
9
- attr_reader :validation
10
- private :validation
8
+ attr_reader :pattern
9
+
10
+ VALIDATORS = {
11
+ email: /^[a-z0-9._%+-]+@([a-z0-9-]+\.)+[a-z]{2,6}$/i
12
+ }
11
13
 
12
14
  # Initialize a Validation
13
15
  #
14
- # @param [Object] validation
16
+ # @param [Object] pattern
15
17
  #
16
18
  # @return [undefined]
17
19
  #
18
20
  # @api private
19
- def initialize(validation = nil)
20
- @validation = validation ? coerce(validation) : validation
21
+ def initialize(pattern)
22
+ @pattern = coerce(pattern)
21
23
  end
22
24
 
23
25
  # Convert validation into known type.
24
26
  #
25
- # @param [Object] validation
27
+ # @param [Object] pattern
26
28
  #
27
- # @raise [TTY::ValidationCoercion] failed to convert validation
29
+ # @raise [TTY::ValidationCoercion]
30
+ # raised when failed to convert validation
28
31
  #
29
32
  # @api private
30
- def coerce(validation)
31
- case validation
32
- when Proc
33
- validation
34
- when Regexp, String
35
- Regexp.new(validation.to_s)
33
+ def coerce(pattern)
34
+ case pattern
35
+ when String, Symbol, Proc
36
+ pattern
37
+ when Regexp
38
+ Regexp.new(pattern.to_s)
36
39
  else
37
- fail ValidationCoercion, "Wrong type, got #{validation.class}"
40
+ fail ValidationCoercion, "Wrong type, got #{pattern.class}"
38
41
  end
39
42
  end
40
43
 
41
- # Check if validation is required
42
- #
43
- # @return [Boolean]
44
- #
45
- # @api public
46
- def validate?
47
- !!validation
48
- end
49
-
50
44
  # Test if the input passes the validation
51
45
  #
52
46
  # @example
53
- # Validation.new
54
- # validation.valid?(input) # => true
47
+ # Validation.new(/pattern/)
48
+ # validation.call(input) # => true
55
49
  #
56
50
  # @param [Object] input
57
51
  # the input to validate
@@ -60,15 +54,15 @@ module TTY
60
54
  #
61
55
  # @api public
62
56
  def call(input)
63
- if validate? && input
64
- input = input.to_s
65
- if validation.is_a?(Regexp) && validation =~ input
66
- elsif validation.is_a?(Proc) && validation.call(input)
67
- else fail InvalidArgument, "Invalid input for #{input}"
68
- end
69
- true
70
- else
71
- false
57
+ if pattern.is_a?(String) || pattern.is_a?(Symbol)
58
+ VALIDATORS.key?(pattern.to_sym)
59
+ !VALIDATORS[pattern.to_sym].match(input).nil?
60
+ elsif pattern.is_a?(Regexp)
61
+ !pattern.match(input).nil?
62
+ elsif pattern.is_a?(Proc)
63
+ result = pattern.call(input)
64
+ result.nil? ? false : result
65
+ else false
72
66
  end
73
67
  end
74
68
  end # Validation
@@ -1,28 +1,37 @@
1
1
  # encoding: utf-8
2
2
 
3
+ require 'wisper'
4
+ require 'tty/prompt/reader/key_event'
5
+ require 'tty/prompt/reader/mode'
6
+
3
7
  module TTY
4
8
  # A class responsible for shell prompt interactions.
5
9
  class Prompt
6
10
  # A class responsible for reading character input from STDIN
7
11
  class Reader
8
- # @api private
9
- attr_reader :prompt
10
- private :prompt
12
+ include Wisper::Publisher
11
13
 
12
14
  attr_reader :mode
13
15
 
16
+ attr_reader :input
17
+
18
+ attr_reader :output
19
+
14
20
  # Key input constants for decimal codes
15
21
  CARRIAGE_RETURN = 13.freeze
16
22
  NEWLINE = 10.freeze
17
23
  BACKSPACE = 127.freeze
18
24
  DELETE = 8.freeze
19
25
 
26
+ CSI = "\e[".freeze
27
+
20
28
  # Initialize a Reader
21
29
  #
22
30
  # @api public
23
- def initialize(prompt = Prompt.new)
24
- @prompt = prompt
25
- @mode = Mode.new
31
+ def initialize(input, output)
32
+ @input = input
33
+ @output = output
34
+ @mode = Mode.new
26
35
  end
27
36
 
28
37
  # Get input in unbuffered mode.
@@ -36,13 +45,11 @@ module TTY
36
45
  #
37
46
  # @api public
38
47
  def buffer(&block)
39
- bufferring = prompt.output.sync
48
+ bufferring = output.sync
40
49
  # Immediately flush output
41
- prompt.output.sync = true
42
-
50
+ output.sync = true
43
51
  value = block.call if block_given?
44
-
45
- prompt.output.sync = bufferring
52
+ output.sync = bufferring
46
53
  value
47
54
  end
48
55
 
@@ -52,11 +59,14 @@ module TTY
52
59
  # @return [String]
53
60
  #
54
61
  # @api public
55
- def read_keypress
62
+ def read_keypress(echo = false)
56
63
  buffer do
57
- mode.echo(false) do
64
+ mode.echo(echo) do
58
65
  mode.raw(true) do
59
- read_char
66
+ key = read_char
67
+ publish_keypress_event(key) if key
68
+ exit 130 if key == Codes::CTRL_C
69
+ key
60
70
  end
61
71
  end
62
72
  end
@@ -68,78 +78,114 @@ module TTY
68
78
  #
69
79
  # @api public
70
80
  def read_char
71
- chars = prompt.input.getc.chr
72
- if chars == "\e"
73
- chars = prompt.input.read_nonblock(3) rescue chars
74
- chars = prompt.input.read_nonblock(2) rescue chars
75
- chars = "\e" + chars
81
+ chars = input.sysread(1)
82
+ while CSI.start_with?(chars) ||
83
+ chars.start_with?(CSI) &&
84
+ !(64..126).include?(chars.each_codepoint.to_a.last)
85
+ next_char = read_char
86
+ chars << next_char
76
87
  end
77
88
  chars
78
- rescue
89
+ rescue EOFError
90
+ # Finished processing
79
91
  chars
80
92
  end
81
93
 
82
- # Get a value from STDIN one key at a time. Each key press is echoed back
83
- # to the shell masked with character(if given). The input finishes when
84
- # enter key is pressed.
94
+ # Get a single line from STDIN
95
+ # Each key pressed is echoed back to the shell.
96
+ # The input terminates when enter or return key is pressed.
85
97
  #
86
- # @param [String] mask
87
- # the character to use as mask
98
+ # @param [Boolean] echo
99
+ # if true echo back characters, output nothing otherwise
88
100
  #
89
101
  # @return [String]
90
102
  #
91
103
  # @api public
92
- def getc(mask = (not_set = true))
93
- value = ''
104
+ def read_line(echo = true)
105
+ line = ''
94
106
  buffer do
95
- begin
96
- while (char = prompt.input.getbyte) &&
107
+ mode.echo(echo) do
108
+ while (char = input.getbyte) &&
97
109
  !(char == CARRIAGE_RETURN || char == NEWLINE)
98
- value = handle_char value, char, not_set, mask
110
+ publish_keypress_event(convert_byte(char))
111
+ line = handle_char(line, char)
99
112
  end
100
- ensure
101
- mode.echo_on
102
113
  end
103
114
  end
104
- value
115
+ line
116
+ end
117
+
118
+ # Read multiple lines and terminate when empty line is submitted.
119
+ #
120
+ # @yield [String] line
121
+ #
122
+ # @return [Array[String]]
123
+ #
124
+ # @api public
125
+ def read_multiline
126
+ response = []
127
+ loop do
128
+ line = read_line
129
+ break if !line || line == ''
130
+ next if line !~ /\S/
131
+ if block_given?
132
+ yield(line)
133
+ else
134
+ response << line
135
+ end
136
+ end
137
+ response
105
138
  end
106
139
 
107
- # Get a value from STDIN using line input.
140
+ # Publish event
141
+ #
142
+ # @param [String] key
143
+ # the key pressed
144
+ #
145
+ # @return [nil]
108
146
  #
109
147
  # @api public
110
- def gets
111
- prompt.input.gets
148
+ def publish_keypress_event(char)
149
+ event = KeyEvent.from(char)
150
+ event_name = parse_key_event(event)
151
+ publish(event_name, event) unless event_name.nil?
152
+ publish(:keypress, event)
112
153
  end
113
154
 
114
- # Reads at maximum +maxlen+ characters.
155
+ # Interpret the key and provide event name
115
156
  #
116
- # @param [Integer] maxlen
157
+ # @return [Symbol]
117
158
  #
118
159
  # @api public
119
- def readpartial(maxlen)
120
- prompt.input.readpartial(maxlen)
160
+ def parse_key_event(event)
161
+ return if event.key.nil?
162
+ permitted_events = %w(up down left right space return enter num)
163
+ return unless permitted_events.include?("#{event.key.name}")
164
+ :"key#{event.key.name}"
121
165
  end
122
166
 
123
167
  private
124
168
 
125
- # Handle single character by appending to or removing from output
169
+ trap('SIGINT') { exit 130 }
170
+
171
+ # Convert byte to unicode character
172
+ #
173
+ # @return [String]
126
174
  #
127
175
  # @api private
128
- def handle_char(value, char, not_set, mask)
129
- if char == BACKSPACE || char == DELETE
130
- value.slice!(-1, 1) unless value.empty?
131
- else
132
- print_char char, not_set, mask
133
- value << char
134
- end
135
- value
176
+ def convert_byte(byte)
177
+ Array(byte).pack('U*')
136
178
  end
137
179
 
138
- # Print out character back to shell STDOUT
180
+ # Handle single character by appending to or removing from output
139
181
  #
140
182
  # @api private
141
- def print_char(char, not_set, mask)
142
- prompt.output.putc((not_set || !mask) ? char : mask)
183
+ def handle_char(line, char)
184
+ if char == BACKSPACE || char == DELETE
185
+ line.empty? ? line : line.slice(-1, 1)
186
+ else
187
+ line << char
188
+ end
143
189
  end
144
190
  end # Reader
145
191
  end # Prompt
@@ -0,0 +1,63 @@
1
+ # encoding: utf-8
2
+
3
+ module TTY
4
+ class Prompt
5
+ class Reader
6
+ module Codes
7
+ BACKSPACE = "\177"
8
+ DELETE = "\004"
9
+ ESCAPE = "\e"
10
+ LINEFEED = "\n"
11
+ RETURN = "\r"
12
+ SPACE = " "
13
+ TAB = "\t"
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"
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"
32
+
33
+ CTRL_J = "\x0A"
34
+ CTRL_N = "\x0E"
35
+ CTRL_K = "\x0B"
36
+ CTRL_P = "\x10"
37
+ SIGINT = "\x03"
38
+ CTRL_C = "\x03"
39
+ CTRL_H = "\b"
40
+ CTRL_L = "\f"
41
+
42
+ F1 = "\eOP"
43
+ F2 = "\eOQ"
44
+ F3 = "\eOR"
45
+ F4 = "\eOS"
46
+
47
+ F1_ALT = "\e[11~"
48
+ F2_ALT = "\e[12~"
49
+ F3_ALT = "\e[13~"
50
+ F4_ALT = "\e[14~"
51
+
52
+ F5 = "\e[15~"
53
+ F6 = "\e[17~"
54
+ F7 = "\e[18~"
55
+ F8 = "\e[19~"
56
+ F9 = "\e[20~"
57
+ F10 = "\e[21~"
58
+ F11 = "\e[23~"
59
+ F12 = "\e[24~"
60
+ end # Codes
61
+ end # Reader
62
+ end # Prompt
63
+ end # TTY