tty 0.0.6 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. data/README.md +78 -12
  2. data/benchmarks/shell.rb +26 -0
  3. data/benchmarks/table.rb +35 -0
  4. data/lib/tty.rb +23 -1
  5. data/lib/tty/coercer.rb +13 -0
  6. data/lib/tty/coercer/boolean.rb +39 -0
  7. data/lib/tty/coercer/float.rb +23 -0
  8. data/lib/tty/coercer/integer.rb +23 -0
  9. data/lib/tty/coercer/range.rb +33 -0
  10. data/lib/tty/shell.rb +6 -2
  11. data/lib/tty/shell/question.rb +158 -138
  12. data/lib/tty/shell/reader.rb +92 -0
  13. data/lib/tty/shell/response.rb +219 -0
  14. data/lib/tty/shell/response_delegation.rb +53 -0
  15. data/lib/tty/table.rb +90 -16
  16. data/lib/tty/table/border.rb +34 -8
  17. data/lib/tty/table/border/ascii.rb +16 -25
  18. data/lib/tty/table/border/null.rb +0 -6
  19. data/lib/tty/table/border/unicode.rb +16 -25
  20. data/lib/tty/table/column_set.rb +1 -1
  21. data/lib/tty/table/error.rb +10 -0
  22. data/lib/tty/table/operation/wrapped.rb +0 -6
  23. data/lib/tty/table/orientation.rb +57 -0
  24. data/lib/tty/table/orientation/horizontal.rb +19 -0
  25. data/lib/tty/table/orientation/vertical.rb +19 -0
  26. data/lib/tty/table/renderer.rb +7 -0
  27. data/lib/tty/table/renderer/ascii.rb +1 -1
  28. data/lib/tty/table/renderer/basic.rb +2 -2
  29. data/lib/tty/table/renderer/unicode.rb +1 -1
  30. data/lib/tty/table/validatable.rb +20 -0
  31. data/lib/tty/terminal.rb +15 -14
  32. data/lib/tty/terminal/color.rb +1 -1
  33. data/lib/tty/terminal/echo.rb +41 -0
  34. data/lib/tty/terminal/home.rb +31 -0
  35. data/lib/tty/text.rb +85 -0
  36. data/lib/tty/text/truncation.rb +83 -0
  37. data/lib/tty/text/wrapping.rb +96 -0
  38. data/lib/tty/version.rb +1 -1
  39. data/spec/tty/coercer/boolean/coerce_spec.rb +113 -0
  40. data/spec/tty/coercer/float/coerce_spec.rb +32 -0
  41. data/spec/tty/coercer/integer/coerce_spec.rb +39 -0
  42. data/spec/tty/coercer/range/coerce_spec.rb +73 -0
  43. data/spec/tty/shell/ask_spec.rb +14 -1
  44. data/spec/tty/shell/question/argument_spec.rb +30 -0
  45. data/spec/tty/shell/question/character_spec.rb +16 -0
  46. data/spec/tty/shell/question/default_spec.rb +25 -0
  47. data/spec/tty/shell/question/in_spec.rb +23 -0
  48. data/spec/tty/shell/question/initialize_spec.rb +11 -211
  49. data/spec/tty/shell/question/modifier/whitespace_spec.rb +1 -1
  50. data/spec/tty/shell/question/modify_spec.rb +44 -0
  51. data/spec/tty/shell/question/valid_spec.rb +46 -0
  52. data/spec/tty/shell/question/validate_spec.rb +30 -0
  53. data/spec/tty/shell/reader/getc_spec.rb +40 -0
  54. data/spec/tty/shell/response/read_bool_spec.rb +41 -0
  55. data/spec/tty/shell/response/read_char_spec.rb +17 -0
  56. data/spec/tty/shell/response/read_date_spec.rb +20 -0
  57. data/spec/tty/shell/response/read_email_spec.rb +43 -0
  58. data/spec/tty/shell/response/read_multiple_spec.rb +24 -0
  59. data/spec/tty/shell/response/read_number_spec.rb +29 -0
  60. data/spec/tty/shell/response/read_range_spec.rb +29 -0
  61. data/spec/tty/shell/response/read_spec.rb +68 -0
  62. data/spec/tty/shell/response/read_string_spec.rb +19 -0
  63. data/spec/tty/table/access_spec.rb +6 -0
  64. data/spec/tty/table/border/new_spec.rb +3 -3
  65. data/spec/tty/table/initialize_spec.rb +17 -1
  66. data/spec/tty/table/options_spec.rb +7 -1
  67. data/spec/tty/table/orientation_spec.rb +98 -0
  68. data/spec/tty/table/renders_with_spec.rb +76 -0
  69. data/spec/tty/table/rotate_spec.rb +72 -0
  70. data/spec/tty/table/to_s_spec.rb +13 -1
  71. data/spec/tty/table/validatable/validate_options_spec.rb +34 -0
  72. data/spec/tty/terminal/color/remove_spec.rb +34 -1
  73. data/spec/tty/terminal/echo_spec.rb +22 -0
  74. data/spec/tty/text/truncate_spec.rb +13 -0
  75. data/spec/tty/text/truncation/initialize_spec.rb +29 -0
  76. data/spec/tty/text/truncation/truncate_spec.rb +73 -0
  77. data/spec/tty/text/wrap_spec.rb +14 -0
  78. data/spec/tty/text/wrapping/initialize_spec.rb +25 -0
  79. data/spec/tty/text/wrapping/wrap_spec.rb +80 -0
  80. data/tty.gemspec +1 -0
  81. metadata +101 -8
@@ -0,0 +1,92 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ module TTY
4
+ class Shell
5
+
6
+ # A class responsible for reading character input from STDIN
7
+ class Reader
8
+
9
+ # @api private
10
+ attr_reader :shell
11
+ private :shell
12
+
13
+ # Key input constants for decimal codes
14
+ CARRIAGE_RETURN = 13.freeze
15
+ NEWLINE = 10.freeze
16
+ BACKSPACE = 127.freeze
17
+ DELETE = 8.freeze
18
+
19
+ # Initialize a Reader
20
+ #
21
+ # @api public
22
+ def initialize(shell=nil)
23
+ @shell = shell || Shell.new
24
+ end
25
+
26
+ # Get input in unbuffered mode.
27
+ #
28
+ #
29
+ # @api bublic
30
+ def buffer(&block)
31
+ bufferring = shell.output.sync
32
+ # Immediately flush output
33
+ shell.output.sync = true
34
+
35
+ value = block.call if block_given?
36
+
37
+ shell.output.sync = bufferring
38
+ value
39
+ end
40
+
41
+ # Get a value from STDIN one key at a time. Each key press is echoed back
42
+ # to the shell masked with character(if given). The input finishes when
43
+ # enter key is pressed.
44
+ #
45
+ # @param [String] mask
46
+ # the character to use as mask
47
+ #
48
+ # @return [String]
49
+ #
50
+ # @api public
51
+ def getc(mask=(not_set=true))
52
+ value = ""
53
+
54
+ buffer do
55
+ begin
56
+ while (char = shell.input.getbyte) and
57
+ !(char == CARRIAGE_RETURN || char == NEWLINE)
58
+
59
+ if (char == BACKSPACE || char == DELETE)
60
+ value.slice!(-1, 1) unless value.empty?
61
+ else
62
+ print_char char, not_set, mask
63
+ value << char
64
+ end
65
+ end
66
+ ensure
67
+ TTY.terminal.echo_on
68
+ end
69
+ end
70
+
71
+ value
72
+ end
73
+
74
+ # Get a value from STDIN using line input.
75
+ #
76
+ # @api public
77
+ def gets
78
+ shell.input.gets
79
+ end
80
+
81
+ private
82
+
83
+ # Print out character back to shell STDOUT
84
+ #
85
+ # @api private
86
+ def print_char(char, not_set, mask)
87
+ shell.output.putc((not_set || !mask) ? char : mask)
88
+ end
89
+
90
+ end # Reader
91
+ end # Shell
92
+ end # TTY
@@ -0,0 +1,219 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'date'
4
+
5
+ module TTY
6
+ # A class responsible for shell prompt interactions.
7
+ class Shell
8
+
9
+ # A class representing a question.
10
+ class Response
11
+
12
+ VALID_TYPES = [:boolean, :string, :symbol, :integer, :float, :date, :datetime]
13
+ attr_reader :reader
14
+ private :reader
15
+
16
+ attr_reader :shell
17
+ private :shell
18
+
19
+ attr_reader :question
20
+ private :question
21
+
22
+ # Initialize a Response
23
+ #
24
+ # @api public
25
+ def initialize(question, shell=nil)
26
+ @question = question
27
+ @shell = shell || Shell.new
28
+ @reader = Reader.new(shell)
29
+ end
30
+
31
+ # Read input from STDIN either character or line
32
+ #
33
+ # @param [Symbol] type
34
+ #
35
+ # @return [undefined]
36
+ #
37
+ # @api public
38
+ def read(type=nil)
39
+ question.evaluate_response read_input
40
+ end
41
+
42
+ # @api private
43
+ def read_input
44
+ reader = Reader.new(shell)
45
+
46
+ if question.mask? && question.echo?
47
+ reader.getc(question.mask)
48
+ else
49
+ TTY.terminal.echo(question.echo) {
50
+ question.character? ? reader.getc(question.mask) : reader.gets
51
+ }
52
+ end
53
+ end
54
+
55
+ # Read answer and cast to String type
56
+ #
57
+ # @param [String] error
58
+ # error to display on failed conversion to string type
59
+ #
60
+ # @api public
61
+ def read_string(error=nil)
62
+ question.evaluate_response String(read_input)
63
+ end
64
+
65
+ # Read answer's first character
66
+ #
67
+ # @api public
68
+ def read_char
69
+ question.character true
70
+ question.evaluate_response String(read_input).chars.to_a[0]
71
+ end
72
+
73
+ # Read multiple line answer and cast to String type
74
+ #
75
+ # @api public
76
+ def read_text
77
+ question.evaluate_response String(read_input)
78
+ end
79
+
80
+ # Read ansewr and cast to Symbol type
81
+ #
82
+ # @api public
83
+ def read_symbol(error=nil)
84
+ question.evaluate_response read_input.to_sym
85
+ end
86
+
87
+ # Read answer from predifined choicse
88
+ #
89
+ # @api public
90
+ def read_choice(type=nil)
91
+ question.argument(:required) unless question.default?
92
+ question.evaluate_response read_input
93
+ end
94
+
95
+ # Read integer value
96
+ #
97
+ # @api public
98
+ def read_int(error=nil)
99
+ question.evaluate_response TTY::Coercer::Integer.coerce(read_input)
100
+ end
101
+
102
+ # Read float value
103
+ #
104
+ # @api public
105
+ def read_float(error=nil)
106
+ question.evaluate_response TTY::Coercer::Float.coerce(read_input)
107
+ end
108
+
109
+ # Read regular expression
110
+ #
111
+ # @api public
112
+ def read_regex(error=nil)
113
+ question.evaluate_response Kernel.send(:Regex, read_input)
114
+ end
115
+
116
+ # Read range expression
117
+ #
118
+ # @api public
119
+ def read_range
120
+ question.evaluate_response TTY::Coercer::Range.coerce(read_input)
121
+ end
122
+
123
+ # Read date
124
+ #
125
+ # @api public
126
+ def read_date
127
+ question.evaluate_response Date.parse(read_input)
128
+ end
129
+
130
+ # Read datetime
131
+ #
132
+ # @api public
133
+ def read_datetime
134
+ question.evaluate_response DateTime.parse(read_input)
135
+ end
136
+
137
+ # Read boolean
138
+ #
139
+ # @api public
140
+ def read_bool(error=nil)
141
+ question.evaluate_response TTY::Coercer::Boolean.coerce read_input
142
+ end
143
+
144
+ # Read file contents
145
+ #
146
+ # @api public
147
+ def read_file(error=nil)
148
+ question.evaluate_response File.open(File.join(directory, read_input))
149
+ end
150
+
151
+ # Read string answer and validate against email regex
152
+ #
153
+ # @return [String]
154
+ #
155
+ # @api public
156
+ def read_email
157
+ question.validate(/^[a-z0-9._%+-]+@([a-z0-9-]+\.)+[a-z]{2,6}$/i)
158
+ if question.error
159
+ question.prompt question.statement
160
+ end
161
+ with_exception { read_string }
162
+ end
163
+
164
+ # Read answer provided on multiple lines
165
+ #
166
+ # @api public
167
+ def read_multiple
168
+ response = ""
169
+ loop do
170
+ value = question.evaluate_response read_input
171
+ break if !value || value == ""
172
+ next if value !~ /\S/
173
+ response << value
174
+ end
175
+ response
176
+ end
177
+
178
+ # Read password
179
+ #
180
+ # @api public
181
+ def read_password
182
+ question.echo false
183
+ question.evaluate_response read_input
184
+ end
185
+
186
+ private
187
+
188
+ # Ignore exception
189
+ #
190
+ # @api private
191
+ def with_exception(&block)
192
+ yield
193
+ rescue
194
+ question.error? ? block.call : raise
195
+ end
196
+
197
+ # @param [Symbol] type
198
+ # :boolean, :string, :numeric, :array
199
+ #
200
+ # @api private
201
+ def read_type(class_or_name)
202
+ raise TypeError, "Type #{type} is not valid" if type && !valid_type?(type)
203
+ case type
204
+ when :string, ::String
205
+ read_string
206
+ when :symbol, ::Symbol
207
+ read_symbol
208
+ when :float, ::Float
209
+ read_float
210
+ end
211
+ end
212
+
213
+ def valid_type?(type)
214
+ self.class::VALID_TYPES.include? type.to_sym
215
+ end
216
+
217
+ end # Response
218
+ end # Shell
219
+ end # TTY
@@ -0,0 +1,53 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ module TTY
4
+ class Shell
5
+ module ResponseDelegation
6
+ extend TTY::Delegatable
7
+
8
+ delegatable_method :dispatch, :read
9
+
10
+ delegatable_method :dispatch, :read_string
11
+
12
+ delegatable_method :dispatch, :read_char
13
+
14
+ delegatable_method :dispatch, :read_text
15
+
16
+ delegatable_method :dispatch, :read_symbol
17
+
18
+ delegatable_method :dispatch, :read_choice
19
+
20
+ delegatable_method :dispatch, :read_int
21
+
22
+ delegatable_method :dispatch, :read_float
23
+
24
+ delegatable_method :dispatch, :read_regex
25
+
26
+ delegatable_method :dispatch, :read_range
27
+
28
+ delegatable_method :dispatch, :read_date
29
+
30
+ delegatable_method :dispatch, :read_datetime
31
+
32
+ delegatable_method :dispatch, :read_bool
33
+
34
+ delegatable_method :dispatch, :read_file
35
+
36
+ delegatable_method :dispatch, :read_email
37
+
38
+ delegatable_method :dispatch, :read_multiple
39
+
40
+ delegatable_method :dispatch, :read_password
41
+
42
+ # Create response instance when question readed is invoked
43
+ #
44
+ # @param [TTY::Shell::Response] response
45
+ #
46
+ # @api private
47
+ def dispatch(response = Response.new(self, shell))
48
+ @response ||= response
49
+ end
50
+
51
+ end # ResponseDelegation
52
+ end # Shell
53
+ end # TTY
@@ -40,16 +40,22 @@ module TTY
40
40
  # @api private
41
41
  attr_reader :alignments
42
42
 
43
+ # The table border class
44
+ #
45
+ # @api private
46
+ attr_reader :border_class
47
+
48
+ # The table orientation out of :horizontal and :vertical
49
+ #
50
+ # @reutnr [TTY::Table::Orientation]
51
+ #
52
+ # @api public
53
+ attr_reader :orientation
54
+
43
55
  # Subset of safe methods that both Array and Hash implement
44
56
  def_delegators(:@rows, :[], :assoc, :flatten, :include?, :index,
45
57
  :length, :select, :to_a, :values_at, :pretty_print, :rassoc)
46
58
 
47
- # The table orientation
48
- #
49
- def direction
50
- # TODO implement table orientation
51
- end
52
-
53
59
  # Create a new Table where each argument is a row
54
60
  #
55
61
  # @example
@@ -74,20 +80,36 @@ module TTY
74
80
  # rows = [ ['a1', 'a2'], ['b1', 'b2'] ]
75
81
  # table = Table.new :header => ['Header 1', 'Header 2'], :rows => rows
76
82
  #
83
+ # @example of parameters passed as hash
84
+ # Table.new [ {'Header1' => ['a1','a2'], 'Header2' => ['b1', 'b2'] }] }
85
+ #
77
86
  # @param [Array[Symbol], Hash] *args
78
87
  #
79
88
  # @api public
80
89
  def self.new(*args, &block)
81
90
  options = Utils.extract_options!(args)
82
91
  if args.size.nonzero?
83
- rows = args.pop
84
- header = args.size.zero? ? nil : args.first
85
- super({:header => header, :rows => rows}.merge(options), &block)
92
+ super(extract_tuples(args).merge(options), &block)
86
93
  else
87
94
  super(options, &block)
88
95
  end
89
96
  end
90
97
 
98
+ # Extract header and row tuples from arguments
99
+ #
100
+ # @param [Array] args
101
+ #
102
+ # @api private
103
+ def self.extract_tuples(args)
104
+ rows = args.pop
105
+ header = args.size.zero? ? nil : args.first
106
+ if rows.first.is_a?(Hash)
107
+ header = rows.map(&:keys).flatten.uniq
108
+ rows = rows.inject([]) { |arr, el| arr + el.values }
109
+ end
110
+ { :header => header, :rows => rows }
111
+ end
112
+
91
113
  # Initialize a Table
92
114
  #
93
115
  # @param [Hash] options
@@ -107,26 +129,78 @@ module TTY
107
129
  #
108
130
  # @api private
109
131
  def initialize(options={}, &block)
110
- @header = options.fetch :header, nil
111
- @rows = coerce(options.fetch :rows, [])
132
+ validate_options! options
133
+
134
+ @header = options.fetch(:header) { nil }
135
+ @rows = coerce options.fetch(:rows) { [] }
112
136
  @renderer = pick_renderer options[:renderer]
137
+ @orientation = Orientation.coerce options.fetch(:orientation) { :horizontal }
113
138
  # TODO: assert that row_size is the same as column widths & aligns
114
- # TODO: this is where column extraction should happen!
115
- @column_widths = options.fetch :column_widths, []
116
- @alignments = Operation::AlignmentSet.new options[:column_aligns] || []
139
+ @column_widths = Array(options.delete(:column_widths)).map(&:to_i)
140
+ @alignments = Operation::AlignmentSet.new Array(options.delete(:column_aligns)).map(&:to_sym)
117
141
 
118
142
  assert_row_sizes @rows
143
+ @orientation.transform(self)
119
144
  yield_or_eval &block if block_given?
120
145
  end
121
146
 
147
+ # Sets table orientation
148
+ #
149
+ # @param [String,Symbol] value
150
+ #
151
+ # @api public
152
+ def orientation=(value)
153
+ @orientation = Orientation.coerce value
154
+ end
155
+
156
+ # Marks this table as rotated
157
+ #
158
+ # @api public
159
+ def rotated?
160
+ @rotated
161
+ end
162
+
163
+ # Rotate the table between vertical and horizontal orientation
164
+ #
165
+ # @return [self]
166
+ #
167
+ # @api private
168
+ def rotate
169
+ orientation.transform(self)
170
+ self
171
+ end
172
+
173
+ # Rotate the table vertically
174
+ #
175
+ # @api private
176
+ def rotate_vertical
177
+ @rows = ([header].compact + rows).transpose
178
+ @header = [] if header
179
+ @rotated = true
180
+ end
181
+
182
+ # Rotate the table horizontally
183
+ #
184
+ # @api private
185
+ def rotate_horizontal
186
+ transposed = rows.transpose
187
+ if header && header.empty?
188
+ @rows = transposed[1..-1]
189
+ @header = transposed[0]
190
+ elsif rotated?
191
+ @rows = transposed
192
+ end
193
+ end
194
+
122
195
  # Lookup element of the table given a row(i) and column(j)
123
196
  #
124
197
  # @api public
125
- def [](i, j)
198
+ def [](i, j=false)
199
+ return row(i) unless j
126
200
  if i >= 0 && j >= 0
127
201
  rows.fetch(i){return nil}[j]
128
202
  else
129
- raise IndexError.new("element at(#{i},#{j}) not found")
203
+ raise TTY::Table::TupleMissing.new(i,j)
130
204
  end
131
205
  end
132
206
  alias at []