looksee 0.0.1

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.
@@ -0,0 +1,9 @@
1
+ Autotest.add_hook :initialize do |at|
2
+ at.add_mapping(/ext\/.*\/(.*)\.[ch]/) do |_, m|
3
+ ["test/test_#{m[1]}_extn.rb"]
4
+ end
5
+ end
6
+
7
+ Autotest.add_hook :run_command do |at|
8
+ system "rake compile"
9
+ end
@@ -0,0 +1,3 @@
1
+ == 0.0.1 2009-07-05
2
+
3
+ * Hi.
@@ -0,0 +1,18 @@
1
+ .autotest
2
+ History.txt
3
+ Manifest.txt
4
+ README.rdoc
5
+ Rakefile
6
+ ext/looksee/extconf.rb
7
+ ext/looksee/looksee.c
8
+ ext/looksee/node-1.9.h
9
+ lib/looksee.rb
10
+ lib/looksee/shortcuts.rb
11
+ lib/looksee/version.rb
12
+ script/console
13
+ script/destroy
14
+ script/generate
15
+ spec/looksee_spec.rb
16
+ spec/spec_helper.rb
17
+ tasks/extconf.rake
18
+ tasks/extconf/looksee.rake
@@ -0,0 +1,118 @@
1
+ = Looksee
2
+
3
+ * http://github.com/oggy/looksee
4
+
5
+ == DESCRIPTION
6
+
7
+ Looksee lets you examine the method lookup path of objects in ways not
8
+ possible in plain ruby.
9
+
10
+ == SYNOPSIS
11
+
12
+ Pop this in your .irbrc :
13
+
14
+ require 'looksee/shortcuts'
15
+
16
+ This defines a method +lp+ ("lookup path") which lets you do:
17
+
18
+ irb(main):001:0> lp []
19
+ => Array
20
+ & concat frozen? push taguri
21
+ * count hash rassoc taguri=
22
+ + cycle include? reject take
23
+ - delete index reject! take_while
24
+ << delete_at indexes replace to_a
25
+ <=> delete_if indices reverse to_ary
26
+ == drop insert reverse! to_s
27
+ [] drop_while inspect reverse_each to_yaml
28
+ []= each join rindex transpose
29
+ assoc each_index last select uniq
30
+ at empty? length shift uniq!
31
+ choice eql? map shuffle unshift
32
+ clear fetch map! shuffle! values_at
33
+ collect fill nitems size yaml_initialize
34
+ collect! find_index pack slice zip
35
+ combination first permutation slice! |
36
+ compact flatten pop sort
37
+ compact! flatten! product sort!
38
+ Enumerable
39
+ all? each_slice first min reverse_each
40
+ any? each_with_index grep min_by select
41
+ collect entries group_by minmax sort
42
+ count enum_cons include? minmax_by sort_by
43
+ cycle enum_slice inject none? take
44
+ detect enum_with_index map one? take_while
45
+ drop find max partition to_a
46
+ drop_while find_all max_by reduce zip
47
+ each_cons find_index member? reject
48
+ Object
49
+ taguri taguri= to_yaml to_yaml_properties to_yaml_style
50
+ Kernel
51
+ == hash object_id
52
+ === id private_methods
53
+ =~ inspect protected_methods
54
+ __id__ instance_eval public_methods
55
+ __send__ instance_exec respond_to?
56
+ class instance_of? send
57
+ clone instance_variable_defined? singleton_methods
58
+ display instance_variable_get taint
59
+ dup instance_variable_set tainted?
60
+ enum_for instance_variables tap
61
+ eql? is_a? to_a
62
+ equal? kind_of? to_enum
63
+ extend method to_s
64
+ freeze methods type
65
+ frozen? nil? untaint
66
+
67
+ It'll also color the methods according to whether they're public,
68
+ protected, private, or overridden. So pretty. You gotta try it.
69
+
70
+ By default, it shows public and protected methods. Add private ones
71
+ like so:
72
+
73
+ lp [], :private => true
74
+ lp [], :private # shortcut
75
+
76
+ Or if you don't want protected:
77
+
78
+ lp [], :protected => false
79
+
80
+ There are variations too. And you can configure things. And you can
81
+ use it as a library without polluting the built-in classes. See:
82
+
83
+ $ ri Looksee
84
+
85
+ Enjoy!
86
+
87
+ == INSTALL
88
+
89
+ gem install looksee
90
+
91
+ == FEATURES/PROBLEMS
92
+
93
+ * Currently only does MRI 1.8, 1.9.
94
+
95
+ == LICENSE
96
+
97
+ (The MIT License)
98
+
99
+ Copyright (c) 2009 George Ogata
100
+
101
+ Permission is hereby granted, free of charge, to any person obtaining
102
+ a copy of this software and associated documentation files (the
103
+ 'Software'), to deal in the Software without restriction, including
104
+ without limitation the rights to use, copy, modify, merge, publish,
105
+ distribute, sublicense, and/or sell copies of the Software, and to
106
+ permit persons to whom the Software is furnished to do so, subject to
107
+ the following conditions:
108
+
109
+ The above copyright notice and this permission notice shall be
110
+ included in all copies or substantial portions of the Software.
111
+
112
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
113
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
114
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
115
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
116
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
117
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
118
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,38 @@
1
+ require 'rubygems'
2
+ gem 'hoe', '>= 2.1.0'
3
+ require 'hoe'
4
+ require 'fileutils'
5
+
6
+ require './lib/looksee/version'
7
+
8
+ Hoe.plugin :newgem
9
+ Hoe.plugin :cucumberfeatures
10
+
11
+ $hoe = Hoe.spec 'looksee' do
12
+ self.developer 'George Ogata', 'george.ogata@gmail.com'
13
+ self.rubyforge_name = self.name # TODO this is default value
14
+ self.description_sections = %w'description synopsis'
15
+ # self.extra_deps = [['activesupport','>= 2.0.2']]
16
+ self.extra_dev_deps = [
17
+ ['newgem', ">= #{::Newgem::VERSION}"],
18
+ ['rspec', '>= 1.2.7'],
19
+ ['mocha', '>= 0.9.5'],
20
+ ]
21
+ end
22
+
23
+ # Configure the clean and clobber tasks.
24
+ require 'rake/clean'
25
+ require 'rbconfig'
26
+ CLEAN.include('**/*.o')
27
+ CLOBBER.include("ext/looksee/looksee.#{Config::CONFIG['DLEXT']}")
28
+
29
+ require 'newgem/tasks' # loads /tasks/*.rake
30
+ Dir['tasks/**/*.rake'].each { |t| load t }
31
+
32
+ desc "Rebuild the gem from scratch."
33
+ task :regem => [:clobber, :gem]
34
+
35
+ # Force build before running specs.
36
+ Rake::Task['spec'].prerequisites << 'extconf:compile'
37
+
38
+ task :default => :spec
@@ -0,0 +1,6 @@
1
+ require 'mkmf'
2
+
3
+ $CPPFLAGS << " -DRUBY_VERSION=#{RUBY_VERSION.tr('.', '')}"
4
+ dir_config("looksee")
5
+
6
+ create_makefile("looksee")
@@ -0,0 +1,126 @@
1
+ #include "ruby.h"
2
+
3
+ #if RUBY_VERSION >= 190
4
+ # include "node-1.9.h"
5
+ # include "ruby/st.h"
6
+ #else
7
+ # include "node.h"
8
+ # include "st.h"
9
+ #endif
10
+
11
+ #if RUBY_VERSION < 187
12
+ # define RCLASS_IV_TBL(c) (RCLASS(c)->iv_tbl)
13
+ # define RCLASS_M_TBL(c) (RCLASS(c)->m_tbl)
14
+ # define RCLASS_SUPER(c) (RCLASS(c)->super)
15
+ #endif
16
+
17
+ /*
18
+ * Return the internal superclass of this class.
19
+ *
20
+ * This is either a Class or "IClass." IClasses represent Modules
21
+ * included in the ancestry, and should be treated as opaque objects
22
+ * in ruby space. Convert the IClass to a Module using #iclass_to_module
23
+ * before using it in ruby.
24
+ */
25
+ VALUE Looksee_internal_superclass(VALUE self, VALUE internal_class) {
26
+ VALUE super = RCLASS_SUPER(internal_class);
27
+ if (!super)
28
+ return Qnil;
29
+ return super;
30
+ }
31
+
32
+ /*
33
+ * Return the internal class of the given object.
34
+ *
35
+ * This is either the object's singleton class, if it exists, or the
36
+ * object's birth class.
37
+ */
38
+ VALUE Looksee_internal_class(VALUE self, VALUE object) {
39
+ return RBASIC(object)->klass;
40
+ }
41
+
42
+ /*
43
+ * Return the class or module that the given internal class
44
+ * represents.
45
+ *
46
+ * If a class is given, this is the class. If an iclass is given,
47
+ * this is the module it represents in the lookup chain.
48
+ */
49
+ VALUE Looksee_internal_class_to_module(VALUE self, VALUE internal_class) {
50
+ if (!SPECIAL_CONST_P(internal_class)) {
51
+ switch (BUILTIN_TYPE(internal_class)) {
52
+ case T_ICLASS:
53
+ return RBASIC(internal_class)->klass;
54
+ case T_CLASS:
55
+ return internal_class;
56
+ }
57
+ }
58
+ rb_raise(rb_eArgError, "not an internal class: %s", RSTRING_PTR(rb_inspect(internal_class)));
59
+ }
60
+
61
+ typedef struct add_method_if_matching_arg {
62
+ VALUE names;
63
+ int visibility;
64
+ } add_method_if_matching_arg_t;
65
+
66
+ #if RUBY_VERSION < 190
67
+ # define VISIBILITY(node) ((node)->nd_noex & NOEX_MASK)
68
+ #else
69
+ # define VISIBILITY(node) ((node)->nd_body->nd_noex & NOEX_MASK)
70
+ #endif
71
+
72
+ static int add_method_if_matching(ID method_name, NODE *body, add_method_if_matching_arg_t *arg) {
73
+ /* This entry is for the internal allocator function. */
74
+ if (method_name == ID_ALLOCATOR)
75
+ return ST_CONTINUE;
76
+
77
+ /* Module#undef_method sets body->nd_body to NULL. */
78
+ if (!body || !body->nd_body)
79
+ return ST_CONTINUE;
80
+
81
+ if (VISIBILITY(body) == arg->visibility)
82
+ rb_ary_push(arg->names, ID2SYM(method_name));
83
+ return ST_CONTINUE;
84
+ }
85
+
86
+ static VALUE internal_instance_methods(VALUE klass, long visibility) {
87
+ add_method_if_matching_arg_t arg;
88
+ arg.names = rb_ary_new();
89
+ arg.visibility = visibility;
90
+ st_foreach(RCLASS_M_TBL(klass), add_method_if_matching, (st_data_t)&arg);
91
+ return arg.names;
92
+ }
93
+
94
+ /*
95
+ * Return the list of public instance methods (as Symbols) of the
96
+ * given internal class.
97
+ */
98
+ VALUE Looksee_internal_public_instance_methods(VALUE self, VALUE klass) {
99
+ return internal_instance_methods(klass, NOEX_PUBLIC);
100
+ }
101
+
102
+ /*
103
+ * Return the list of protected instance methods (as Symbols) of the
104
+ * given internal class.
105
+ */
106
+ VALUE Looksee_internal_protected_instance_methods(VALUE self, VALUE klass) {
107
+ return internal_instance_methods(klass, NOEX_PROTECTED);
108
+ }
109
+
110
+ /*
111
+ * Return the list of private instance methods (as Symbols) of the
112
+ * given internal class.
113
+ */
114
+ VALUE Looksee_internal_private_instance_methods(VALUE self, VALUE klass) {
115
+ return internal_instance_methods(klass, NOEX_PRIVATE);
116
+ }
117
+
118
+ void Init_looksee(void) {
119
+ VALUE mLooksee = rb_define_module("Looksee");
120
+ rb_define_singleton_method(mLooksee, "internal_superclass", Looksee_internal_superclass, 1);
121
+ rb_define_singleton_method(mLooksee, "internal_class", Looksee_internal_class, 1);
122
+ rb_define_singleton_method(mLooksee, "internal_class_to_module", Looksee_internal_class_to_module, 1);
123
+ rb_define_singleton_method(mLooksee, "internal_public_instance_methods", Looksee_internal_public_instance_methods, 1);
124
+ rb_define_singleton_method(mLooksee, "internal_protected_instance_methods", Looksee_internal_protected_instance_methods, 1);
125
+ rb_define_singleton_method(mLooksee, "internal_private_instance_methods", Looksee_internal_private_instance_methods, 1);
126
+ }
@@ -0,0 +1,35 @@
1
+ /* MRI 1.9 does not install node.h. This is the part we need. */
2
+
3
+ typedef struct RNode {
4
+ unsigned long flags;
5
+ char *nd_file;
6
+ union {
7
+ struct RNode *node;
8
+ ID id;
9
+ VALUE value;
10
+ VALUE (*cfunc)(ANYARGS);
11
+ ID *tbl;
12
+ } u1;
13
+ union {
14
+ struct RNode *node;
15
+ ID id;
16
+ long argc;
17
+ VALUE value;
18
+ } u2;
19
+ union {
20
+ struct RNode *node;
21
+ ID id;
22
+ long state;
23
+ struct global_entry *entry;
24
+ long cnt;
25
+ VALUE value;
26
+ } u3;
27
+ } NODE;
28
+
29
+ #define nd_body u2.node
30
+ #define nd_noex u3.id
31
+
32
+ #define NOEX_PUBLIC 0x00
33
+ #define NOEX_PRIVATE 0x02
34
+ #define NOEX_PROTECTED 0x04
35
+ #define NOEX_MASK 0x06
@@ -0,0 +1,332 @@
1
+ require "rbconfig"
2
+ require File.dirname(__FILE__) + "/../ext/looksee/looksee.#{Config::CONFIG['DLEXT']}"
3
+ require "looksee/version"
4
+
5
+ #
6
+ # Looksee lets you inspect the method lookup path of an object. There
7
+ # are two ways to use it:
8
+ #
9
+ # 1. Keep all methods contained in the Looksee namespace:
10
+ #
11
+ # require 'looksee'
12
+ #
13
+ # 2. Let it all hang out:
14
+ #
15
+ # require 'looksee/shortcuts'
16
+ #
17
+ # The latter adds the following shortcuts to the built-in classes:
18
+ #
19
+ # Object#lookup_path
20
+ # Object#dump_lookup_path
21
+ # Object#lp
22
+ # Object#lpi
23
+ #
24
+ # See their docs.
25
+ #
26
+ # == Usage
27
+ #
28
+ # In irb:
29
+ #
30
+ # require 'looksee/shortcuts'
31
+ # lp some_object
32
+ #
33
+ # +lp+ returns a LookupPath object, which has +inspect+ defined to
34
+ # print things out pretty. By default, it shows public, protected,
35
+ # and overridden methods. They're all colored, which makes showing
36
+ # overridden methods not such a strange idea.
37
+ #
38
+ # Some examples of the other shortcuts:
39
+ #
40
+ # lpi Array
41
+ # some_object.lookup_path
42
+ # foo.bar.baz.dump_lookup_path.and.more
43
+ #
44
+ # If you're being namespace-clean, you'll need to do:
45
+ #
46
+ # require 'looksee'
47
+ # Looksee.lookup_path(thing) # like "lp thing"
48
+ #
49
+ # == Configuration
50
+ #
51
+ # Set these:
52
+ #
53
+ # Looksee.default_lookup_path_options
54
+ # Looksee.default_width
55
+ # Looksee.styles
56
+ #
57
+ # See their docs.
58
+ #
59
+ module Looksee
60
+ class << self
61
+ #
62
+ # Return a collection of methods that +object+ responds to,
63
+ # according to the options given. The following options are
64
+ # recognized:
65
+ #
66
+ # * +:public+ - include public methods
67
+ # * +:protected+ - include protected methods
68
+ # * +:private+ - include private methods
69
+ # * +:overridden+ - include methods overridden by subclasses
70
+ #
71
+ # The default (if options is nil or omitted) is [:public].
72
+ #
73
+ def lookup_path(object, *options)
74
+ normalized_options = Looksee.default_lookup_path_options.dup
75
+ hash_options = options.last.is_a?(Hash) ? options.pop : {}
76
+ options.each do |option|
77
+ normalized_options[option] = true
78
+ end
79
+ normalized_options.update(hash_options)
80
+ LookupPath.new(object, normalized_options)
81
+ end
82
+
83
+ #
84
+ # The default options passed to lookup_path.
85
+ #
86
+ # Default: <tt>{:public => true, :protected => true, :overridden => true}</tt>
87
+ #
88
+ attr_accessor :default_lookup_path_options
89
+
90
+ #
91
+ # The width to use for displaying output, when not available in
92
+ # the COLUMNS environment variable.
93
+ #
94
+ # Default: 80
95
+ #
96
+ attr_accessor :default_width
97
+
98
+ #
99
+ # The default styles to use for the +inspect+ strings.
100
+ #
101
+ # This is a hash with keys:
102
+ #
103
+ # * :module
104
+ # * :public
105
+ # * :protected
106
+ # * :private
107
+ # * :overridden
108
+ #
109
+ # The values are format strings. They should all contain a single
110
+ # "%s", which is where the name is inserted.
111
+ #
112
+ # Default:
113
+ #
114
+ # {
115
+ # :module => "\e[1;37m%s\e[0m",
116
+ # :public => "\e[1;32m%s\e[0m",
117
+ # :protected => "\e[1;33m%s\e[0m",
118
+ # :private => "\e[1;31m%s\e[0m",
119
+ # :overridden => "\e[1;30m%s\e[0m",
120
+ # }
121
+ #
122
+ attr_accessor :styles
123
+
124
+ #
125
+ # Return the chain of classes and modules which comprise the
126
+ # object's method lookup path.
127
+ #
128
+ def lookup_modules(object)
129
+ modules = []
130
+ klass = Looksee.internal_class(object)
131
+ while klass
132
+ modules << Looksee.internal_class_to_module(klass)
133
+ klass = Looksee.internal_superclass(klass)
134
+ end
135
+ modules
136
+ end
137
+ end
138
+
139
+ self.default_lookup_path_options = {:public => true, :protected => true, :overridden => true}
140
+ self.default_width = 80
141
+ self.styles = {
142
+ :module => "\e[1;37m%s\e[0m",
143
+ :public => "\e[1;32m%s\e[0m",
144
+ :protected => "\e[1;33m%s\e[0m",
145
+ :private => "\e[1;31m%s\e[0m",
146
+ :overridden => "\e[1;30m%s\e[0m",
147
+ }
148
+
149
+ class LookupPath
150
+ attr_reader :entries
151
+
152
+ #
153
+ # Create a LookupPath for the given object.
154
+ #
155
+ # Options may be given to restrict which visibilities are
156
+ # included.
157
+ #
158
+ # :public
159
+ # :protected
160
+ # :private
161
+ # :overridden
162
+ #
163
+ def initialize(object, options={})
164
+ @entries = []
165
+ seen = {}
166
+ Looksee.lookup_modules(object).each do |mod|
167
+ entry = Entry.new(mod, seen, options)
168
+ entry.methods.each{|m| seen[m] = true}
169
+ @entries << entry
170
+ end
171
+ end
172
+
173
+ def inspect(options={})
174
+ options = normalize_inspect_options(options)
175
+ entries.map{|e| e.inspect(options)}.join
176
+ end
177
+
178
+ private # -------------------------------------------------------
179
+
180
+ def normalize_inspect_options(options)
181
+ options[:width] ||= ENV['COLUMNS'].to_i.nonzero? || Looksee.default_width
182
+ options
183
+ end
184
+
185
+ #
186
+ # An entry in the LookupPath.
187
+ #
188
+ # Contains a module and its methods, along with visibility
189
+ # information (public, private, etc.).
190
+ #
191
+ class Entry
192
+ #
193
+ # Don't call me, silly. I'm just part of a LookupPath.
194
+ #
195
+ def initialize(mod, seen, options)
196
+ @module = mod
197
+ @methods = []
198
+ @visibilities = {}
199
+ add_methods(Looksee.internal_public_instance_methods(mod).map{|sym| sym.to_s} , :public , seen) if options[:public ]
200
+ add_methods(Looksee.internal_protected_instance_methods(mod).map{|sym| sym.to_s}, :protected, seen) if options[:protected]
201
+ add_methods(Looksee.internal_private_instance_methods(mod).map{|sym| sym.to_s} , :private , seen) if options[:private ]
202
+ @methods.sort!
203
+ end
204
+
205
+ attr_reader :module, :methods
206
+
207
+ #
208
+ # Return the name of the class or module.
209
+ #
210
+ # Singleton classes are displayed in brackets. Singleton class
211
+ # of singleton classes are displayed in double brackets. But
212
+ # you'd never need that, would you?
213
+ #
214
+ def module_name
215
+ name = @module.to_s # #name doesn't do singleton classes right
216
+ nil while name.sub!(/#<Class:(.*)>/, '[\\1]')
217
+ name
218
+ end
219
+
220
+ #
221
+ # Yield each method along with its visibility (:public,
222
+ # :private, :protected, or :overridden).
223
+ #
224
+ def each
225
+ @methods.each do |name|
226
+ yield name, @visibilities[name]
227
+ end
228
+ end
229
+
230
+ include Enumerable
231
+
232
+ #
233
+ # Return a nice, pretty string for inspection.
234
+ #
235
+ # Contains the module name, plus the method names laid out in
236
+ # columns. Pass a :width option to control the output width.
237
+ #
238
+ def inspect(options={})
239
+ styled_module_name << "\n" << Columnizer.columnize(styled_methods, options[:width])
240
+ end
241
+
242
+ private # -----------------------------------------------------
243
+
244
+ def add_methods(methods, visibility, seen)
245
+ methods.each do |method|
246
+ @methods << method
247
+ @visibilities[method] = seen[method] ? :overridden : visibility
248
+ end
249
+ end
250
+
251
+ def styled_module_name
252
+ Looksee.styles[:module] % module_name
253
+ end
254
+
255
+ def styled_methods
256
+ map do |name, visibility|
257
+ Looksee.styles[visibility] % name
258
+ end
259
+ end
260
+ end
261
+ end
262
+
263
+ module Columnizer
264
+ class << self
265
+ #
266
+ # Arrange the given strings in columns, restricted to the given
267
+ # width. Smart enough to ignore content in terminal control
268
+ # sequences.
269
+ #
270
+ def columnize(strings, width)
271
+ num_columns = 1
272
+ layout = [strings]
273
+ loop do
274
+ break if layout.first.length <= 1
275
+ next_layout = layout_in_columns(strings, num_columns + 1)
276
+ break if layout_width(next_layout) > width
277
+ layout = next_layout
278
+ num_columns += 1
279
+ end
280
+
281
+ pad_strings(layout)
282
+ rectangularize_layout(layout)
283
+ layout.transpose.map do |row|
284
+ ' ' + row.compact.join(' ')
285
+ end.join("\n") << "\n"
286
+ end
287
+
288
+ private # -----------------------------------------------------
289
+
290
+ def layout_in_columns(strings, num_columns)
291
+ strings_per_column = (strings.length / num_columns.to_f).ceil
292
+ (0...num_columns).map{|i| strings[i*strings_per_column...(i+1)*strings_per_column] || []}
293
+ end
294
+
295
+ def layout_width(layout)
296
+ widths = layout_column_widths(layout)
297
+ widths.inject(0){|sum, w| sum + w} + 2*layout.length
298
+ end
299
+
300
+ def layout_column_widths(layout)
301
+ layout.map do |column|
302
+ column.map{|string| display_width(string)}.max || 0
303
+ end
304
+ end
305
+
306
+ def display_width(string)
307
+ # remove terminal control sequences
308
+ string.gsub(/\e\[.*?m/, '').length
309
+ end
310
+
311
+ def pad_strings(layout)
312
+ widths = layout_column_widths(layout)
313
+ layout.each_with_index do |column, i|
314
+ column_width = widths[i]
315
+ column.each do |string|
316
+ padding = column_width - display_width(string)
317
+ string << ' '*padding
318
+ end
319
+ end
320
+ end
321
+
322
+ def rectangularize_layout(layout)
323
+ return if layout.length == 1
324
+ height = layout[0].length
325
+ layout[1..-1].each do |column|
326
+ column.length == height or
327
+ column[height - 1] = nil
328
+ end
329
+ end
330
+ end
331
+ end
332
+ end