tty 0.0.5 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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