oggy-looksee 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.autotest ADDED
@@ -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
data/History.txt ADDED
@@ -0,0 +1,3 @@
1
+ == 0.0.1 2009-07-05
2
+
3
+ * Hi.
data/Manifest.txt ADDED
@@ -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
data/README.rdoc ADDED
@@ -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.
data/Rakefile ADDED
@@ -0,0 +1,37 @@
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.extra_deps = [['activesupport','>= 2.0.2']]
15
+ self.extra_dev_deps = [
16
+ ['newgem', ">= #{::Newgem::VERSION}"],
17
+ ['rspec', '>= 1.2.7'],
18
+ ['mocha', '>= 0.9.5'],
19
+ ]
20
+ end
21
+
22
+ # Configure the clean and clobber tasks.
23
+ require 'rake/clean'
24
+ require 'rbconfig'
25
+ CLEAN.include('**/*.o')
26
+ CLOBBER.include("ext/looksee/looksee.#{Config::CONFIG['DLEXT']}")
27
+
28
+ require 'newgem/tasks' # loads /tasks/*.rake
29
+ Dir['tasks/**/*.rake'].each { |t| load t }
30
+
31
+ desc "Rebuild the gem from scratch."
32
+ task :regem => [:clobber, :gem]
33
+
34
+ # Force build before running specs.
35
+ Rake::Task['spec'].prerequisites << 'extconf:compile'
36
+
37
+ 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
data/lib/looksee.rb ADDED
@@ -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