tty 0.0.6 → 0.0.7

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.
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 []