tty 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -4,18 +4,20 @@
4
4
  [travis]: http://travis-ci.org/peter-murach/tty
5
5
  [codeclimate]: https://codeclimate.com/github/peter-murach/tty
6
6
 
7
- Toolbox for developing CLI clients in Ruby.
7
+ Toolbox for developing CLI clients in Ruby. This library provides a fluid interface for working with terminals.
8
8
 
9
9
  ## Features
10
10
 
11
11
  Jump-start development of your command line app:
12
12
 
13
13
  * Fully customizable table rendering with an easy-to-use API. (status: In Progress)
14
- * Terminal output colorization. (status: TODO)
14
+ * Terminal output colorization. (status: DONE)
15
15
  * Terminal & System detection utilities. (status: In Progress)
16
- * Text alignment/padding and diffs. (status: TODO)
17
- * Shell user interface. (status: TODO)
18
- * Progress bar. (status: TODO)
16
+ * Text alignment/padding/indentation. (status: In Progress)
17
+ * Shell user interface. (status: In Progress)
18
+ * File diffs. (status: TODO)
19
+ * Progress bar. (status: TODO)
20
+ * Configuration file management. (status: TODO)
19
21
  * Fully tested with major ruby interpreters.
20
22
  * No dependencies to allow for easy gem vendoring.
21
23
 
@@ -113,10 +115,63 @@ To print border around data table you need to specify `renderer` type out of `ba
113
115
  term.color? # => true or false
114
116
  ```
115
117
 
118
+ To colorize your output do
119
+
120
+ ```ruby
121
+ term.color.set 'text...', :bold, :red, :on_green # => red bold text on green background
122
+ term.color.remove 'text...' # strips off ansi escape sequences
123
+ term.color.code :red # ansi escape code for the supplied color
124
+ ```
125
+
116
126
  ### Shell
117
127
 
118
128
  Main responsibility is to interact with the prompt and provide convenience methods.
119
129
 
130
+ In order to ask question and parse answers:
131
+
132
+ ```ruby
133
+ shell = TTY::Shell.new
134
+ answer = shell.ask("What is your name?").read_string
135
+ ```
136
+
137
+ The library provides small DSL to help with parsing and asking precise questions
138
+
139
+ ```ruby
140
+ default # default value used if none is provided
141
+ argument # :required or :optional
142
+ validate # regex against which stdin input is checked
143
+ valid # a list of expected valid options
144
+ modify # apply answer modification :upcase, :downcase, :trim, :chomp etc..
145
+ clean # reset question
146
+ ```
147
+
148
+ You can chain question methods or configure them inside a block
149
+
150
+ ```ruby
151
+ shell.ask("What is your name?").argument(:required).default('Piotr').validate(/\w+\s\w+/).read_string
152
+
153
+ shell.ask "What is your name?" do
154
+ argument :required
155
+ default 'Piotr'
156
+ validate /\w+\s\w+/
157
+ valid ['Piotr', 'Piotrek']
158
+ modify :capitalize
159
+ end.read_string
160
+ ```
161
+
162
+ Reading answers and converting them into required types can be done with custom readers
163
+
164
+ ```ruby
165
+ read_string # return string
166
+ read_bool # return true or false for strings such as "Yes", "No"
167
+ read_int # return integer or error if cannot convert
168
+ read_float # return decimal or error if cannot convert
169
+ read_date # return date type
170
+ read_datetime # return datetime type
171
+ read_multiple # return multiple line string
172
+ read_email # validate answer against email regex
173
+ ```
174
+
120
175
  ### System
121
176
 
122
177
  ```ruby
@@ -0,0 +1,96 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ module TTY
4
+ class Shell
5
+ class Question
6
+
7
+ # A class representing String modifications.
8
+ class Modifier
9
+
10
+ attr_reader :modifiers
11
+ private :modifiers
12
+
13
+ # Initialize a Modifier
14
+ #
15
+ # @api public
16
+ def initialize(*modifiers)
17
+ @modifiers = Array(modifiers)
18
+ end
19
+
20
+ # Change supplied value according to the given string transformation.
21
+ # Valid settings are:
22
+ #
23
+ # @param [String] value
24
+ # the string to be modified
25
+ #
26
+ # @return [String]
27
+ #
28
+ # @api private
29
+ def apply_to(value)
30
+ modifiers.inject(value) do |result, mod|
31
+ result = Modifier.letter_case mod, result
32
+ result = Modifier.whitespace mod, result
33
+ end
34
+ end
35
+
36
+ # Changes letter casing in a string according to valid modifications.
37
+ # For invalid modification option the string is preserved.
38
+ #
39
+ # @param [Symbol] mod
40
+ # the modification to change the string
41
+ #
42
+ # @option mod [Symbol] :up change to upper case
43
+ # @option mod [Symbol] :upcase change to upper case
44
+ # @option mod [Symbol] :uppercase change to upper case
45
+ # @option mod [Symbol] :down change to lower case
46
+ # @option mod [Symbol] :downcase change to lower case
47
+ # @option mod [Symbol] :capitalize change all words to start with uppercase case letter
48
+ #
49
+ # @return [String]
50
+ #
51
+ # @api public
52
+ def self.letter_case(mod, value)
53
+ case mod
54
+ when :up, :upcase, :uppercase
55
+ value.upcase
56
+ when :down, :downcase, :lowercase
57
+ value.downcase
58
+ when :capitalize
59
+ value.capitalize
60
+ else
61
+ value
62
+ end
63
+ end
64
+
65
+ # Changes whitespace in a string according to valid modifications.
66
+ #
67
+ # @param [Symbol] mod
68
+ # the modification to change the string
69
+ #
70
+ # @option mod [String] :trim, :strip
71
+ # remove whitespace for the start and end
72
+ # @option mod [String] :chomp remove record separator from the end
73
+ # @option mod [String] :collapse remove any duplicate whitespace
74
+ # @option mod [String] :remove remove all whitespace
75
+ #
76
+ # @api public
77
+ def self.whitespace(mod, value)
78
+ case mod
79
+ when :trim, :strip
80
+ value.strip
81
+ when :chomp
82
+ value.chomp
83
+ when :collapse
84
+ value.gsub(/\s+/, ' ')
85
+ when :remove
86
+ value.gsub(/\s+/, '')
87
+ else
88
+ value
89
+ end
90
+ end
91
+
92
+ end # Modifier
93
+
94
+ end # Question
95
+ end # Shell
96
+ end # TTY
@@ -0,0 +1,91 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ module TTY
4
+ class Shell
5
+ class Question
6
+
7
+ # A class representing question validation.
8
+ class Validation
9
+
10
+ # @api private
11
+ attr_reader :validation
12
+ private :validation
13
+
14
+ # Initialize a Validation
15
+ #
16
+ # @param [Object] validation
17
+ #
18
+ # @return [undefined]
19
+ #
20
+ # @api private
21
+ def initialize(validation=nil)
22
+ @validation = validation ? coerce(validation) : validation
23
+ end
24
+
25
+ # Convert validation into known type.
26
+ #
27
+ # @param [Object] validation
28
+ #
29
+ # @raise [TTY::ValidationCoercion] failed to convert validation
30
+ #
31
+ # @api private
32
+ def coerce(validation)
33
+ case validation
34
+ when Proc
35
+ validation
36
+ when Regexp, String
37
+ Regexp.new(validation.to_s)
38
+ else
39
+ raise TTY::ValidationCoercion, "Wrong type, got #{validation.class}"
40
+ end
41
+ end
42
+
43
+ # Check if validation is required
44
+ #
45
+ # @return [Boolean]
46
+ #
47
+ # @api public
48
+ def validate?
49
+ !!validation
50
+ end
51
+
52
+ # Test if the value matches the validation
53
+ #
54
+ # @example
55
+ # validation.valid_value?(value) # => true or false
56
+ #
57
+ # @param [Object] value
58
+ # the value to validate
59
+ #
60
+ # @return [undefined]
61
+ #
62
+ # @api public
63
+ def valid_value?(value)
64
+ check_validation(value)
65
+ end
66
+
67
+ private
68
+
69
+ # Check if provided value passes validation
70
+ #
71
+ # @param [String] value
72
+ #
73
+ # @raise [TTY::InvalidArgument] unkown type of argument
74
+ #
75
+ # @return [undefined]
76
+ #
77
+ # @api private
78
+ def check_validation(value)
79
+ if validate? && value
80
+ value = value.to_s
81
+ if validation.is_a?(Regexp) && validation =~ value
82
+ elsif validation.is_a?(Proc) && validation.call(value)
83
+ else raise TTY::InvalidArgument, "Invalid input for #{value}"
84
+ end
85
+ end
86
+ end
87
+
88
+ end # Validation
89
+ end # Question
90
+ end # Shell
91
+ end # TTY
@@ -0,0 +1,304 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ module TTY
4
+ class Shell
5
+
6
+ # A class representing a question.
7
+ class Question
8
+
9
+ PREFIX = " + "
10
+ MULTIPLE_PREFIX = " * "
11
+ ERROR_PREFIX = " ERROR:"
12
+
13
+ VALID_TYPES = [:boolean, :string, :symbol, :integer, :float, :date, :datetime]
14
+
15
+ # Store question.
16
+ #
17
+ # @api private
18
+ attr_accessor :question
19
+
20
+ # Store default value.
21
+ #
22
+ # @api private
23
+ attr_reader :default_value
24
+
25
+ attr_reader :required
26
+ private :required
27
+
28
+ attr_reader :validation
29
+
30
+ # Controls character processing to the answer
31
+ #
32
+ # @api public
33
+ attr_reader :modifier
34
+
35
+ attr_reader :valid_values
36
+
37
+ attr_reader :error
38
+
39
+ attr_reader :statement
40
+
41
+ # Expected answer type
42
+ #
43
+ # @api private
44
+ attr_reader :type
45
+
46
+ # @api private
47
+ attr_reader :shell
48
+ private :shell
49
+
50
+ def initialize(shell, statement, options={})
51
+ @shell = shell || Shell.new
52
+ @statement = statement
53
+ @required = options.fetch :required, false
54
+ @modifier = Modifier.new options.fetch(:modifier, [])
55
+ @valid_values = options.fetch :valid, []
56
+ @validation = Validation.new options.fetch(:validation, nil)
57
+ end
58
+
59
+ # Check if required argument present.
60
+ #
61
+ # @api private
62
+ def required?
63
+ required
64
+ end
65
+
66
+ # Set a new prompt
67
+ #
68
+ # @param [String] message
69
+ #
70
+ # @return [self]
71
+ #
72
+ def prompt(message)
73
+ self.question = message
74
+ shell.say question
75
+ self
76
+ end
77
+
78
+ # Set default value.
79
+ #
80
+ # @api public
81
+ def default(value)
82
+ return self if value == ""
83
+ @default_value = value
84
+ self
85
+ end
86
+
87
+ def default?
88
+ !!@default_value
89
+ end
90
+
91
+ # Ensure that passed argument is present if required option
92
+ #
93
+ # @return [Question]
94
+ #
95
+ # @api public
96
+ def argument(value)
97
+ case value
98
+ when :required
99
+ @required = true
100
+ when :optional
101
+ @required = false
102
+ end
103
+ self
104
+ end
105
+
106
+ # Set validation rule for an argument
107
+ #
108
+ # @param [Object] value
109
+ #
110
+ # @return [Question]
111
+ #
112
+ # @api public
113
+ def validate(value=nil, &block)
114
+ @validation = Validation.new(value || block)
115
+ self
116
+ end
117
+
118
+ # @api public
119
+ def valid(value)
120
+ @valid_values = value
121
+ self
122
+ end
123
+
124
+ # @api private
125
+ def check_valid(value)
126
+ if Array(value).all? { |val| @valid_values.include? val }
127
+ return value
128
+ else raise InvalidArgument, "Valid values are: #{@valid_values.join(', ')}"
129
+ end
130
+ end
131
+
132
+ # Reset question object.
133
+ #
134
+ # @api public
135
+ def clean
136
+ @question = nil
137
+ @type = nil
138
+ @default_value = nil
139
+ @required = false
140
+ @modifier = nil
141
+ end
142
+
143
+ # Modify string according to the rule given.
144
+ #
145
+ # @param [Symbol] rule
146
+ #
147
+ # @api public
148
+ def modify(*rules)
149
+ @modifier = Modifier.new *rules
150
+ self
151
+ end
152
+
153
+ # @api public
154
+ def on_error(action=nil)
155
+ @error = action
156
+ self
157
+ end
158
+
159
+ # @api private
160
+ def read(type=nil)
161
+ result = shell.input.gets
162
+ if !result && default?
163
+ return default_value
164
+ end
165
+ if required? && !default? && !result
166
+ raise ArgumentRequired, 'No value provided for required'
167
+ end
168
+ validation.valid_value? result
169
+ modifier.apply_to result
170
+ end
171
+
172
+ # Read answer and cast to String type
173
+ #
174
+ # @param [String] error
175
+ # error to display on failed conversion to string type
176
+ #
177
+ # @api public
178
+ def read_string(error=nil)
179
+ String(read)
180
+ end
181
+
182
+ # Read multiple line answer and cast to String type
183
+ def read_text
184
+ String(read)
185
+ end
186
+
187
+ # Read ansewr and cast to Symbol type
188
+ def read_symbol(error=nil)
189
+ read.to_sym
190
+ end
191
+
192
+ def read_int(error=nil)
193
+ Kernel.send(:Integer, read)
194
+ end
195
+
196
+ def read_float(error=nil)
197
+ Kernel.send(:Float, read)
198
+ end
199
+
200
+ def read_regex(error=nil)
201
+ Kernel.send(:Regex, read)
202
+ end
203
+
204
+ def read_date
205
+ Date.parse(read)
206
+ end
207
+
208
+ def read_datetime
209
+ DateTime.parse(read)
210
+ end
211
+
212
+ def read_bool(error=nil)
213
+ parse_boolean read
214
+ end
215
+
216
+ def read_choice(type=nil)
217
+ @required = true unless default?
218
+ check_valid read
219
+ end
220
+
221
+ def read_file(error=nil)
222
+ File.open(File.join(directory, read))
223
+ end
224
+
225
+ # Ignore exception
226
+ #
227
+ # @api private
228
+ def with_exception(&block)
229
+ yield
230
+ rescue
231
+ block.call
232
+ end
233
+
234
+ # Reads string answer and validates against email regex
235
+ #
236
+ # @return [String]
237
+ #
238
+ # @api public
239
+ def read_email
240
+ validate(/^[a-z0-9._%+-]+@([a-z0-9-]+\.)+[a-z]{2,6}$/i)
241
+ if error
242
+ self.prompt statement
243
+ with_exception { read_string }
244
+ else
245
+ read_string
246
+ end
247
+ end
248
+
249
+ # Read answer provided on multiple lines
250
+ #
251
+ # @api public
252
+ def read_multiple
253
+ response = ""
254
+ loop do
255
+ value = read
256
+ break if !value || value == ""
257
+ response << value
258
+ end
259
+ response
260
+ end
261
+
262
+ protected
263
+
264
+ # @param [Symbol] type
265
+ # :boolean, :string, :numeric, :array
266
+ #
267
+ # @api private
268
+ def read_type(type)
269
+ raise TypeError, "Type #{type} is not valid" if type && !valid_type?(type)
270
+ case type
271
+ when :string
272
+ read_string
273
+ when :symbol
274
+ read_symbol
275
+ when :float
276
+ read_float
277
+ end
278
+ end
279
+
280
+ def valid_type?(type)
281
+ self.class::VALID_TYPES.include? type.to_sym
282
+ end
283
+
284
+ # Convert message into boolean type
285
+ #
286
+ # @param [String] message
287
+ #
288
+ # @return [Boolean]
289
+ #
290
+ # @api private
291
+ def parse_boolean(message)
292
+ case message.to_s
293
+ when %r/^(yes|y)$/i
294
+ return true
295
+ when %r/^(no|n)$/i
296
+ return false
297
+ else
298
+ raise TypeError, "Expected boolean type, got #{message}"
299
+ end
300
+ end
301
+
302
+ end # Question
303
+ end # Shell
304
+ end # TTY
@@ -0,0 +1,55 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ module TTY
4
+ class Shell
5
+
6
+ # A class representing a statement output to shell.
7
+ class Statement
8
+
9
+ # @api private
10
+ attr_reader :shell
11
+ private :shell
12
+
13
+ attr_reader :newline
14
+
15
+ attr_reader :color
16
+
17
+ # Initialize a Statement
18
+ #
19
+ # @param [TTY::Shell] shell
20
+ #
21
+ # @param [Hash] options
22
+ #
23
+ # @option options [Symbol] :newline
24
+ # force a newline break after the message
25
+ #
26
+ # @option options [Symbol] :color
27
+ # change the message display to color
28
+ #
29
+ # @api public
30
+ def initialize(shell=nil, options={})
31
+ @shell = shell || Shell.new
32
+ @newline = options.fetch :newline, true
33
+ @color = options.fetch :color, nil
34
+ end
35
+
36
+ # Output the message to the shell
37
+ #
38
+ # @param [String] message
39
+ # the message to be printed to stdout
40
+ #
41
+ # @api public
42
+ def declare(message)
43
+ message = TTY::terminal.color.set message, *color if color
44
+
45
+ if newline && /( |\t)(\e\[\d+(;\d+)*m)?\Z/ !~ message
46
+ shell.output.puts message
47
+ else
48
+ shell.output.print message
49
+ shell.output.flush
50
+ end
51
+ end
52
+
53
+ end # Statement
54
+ end # Shell
55
+ end # TTY