terminal-table 1.7.1 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -1,8 +1,14 @@
1
- require "bundler/gem_tasks"
1
+ require 'bundler'
2
+ Bundler.setup
3
+ Bundler::GemHelper.install_tasks
2
4
 
3
- desc "Run specs"
4
- task :spec do
5
- sh "bundle exec rspec -f progress"
5
+ require 'rake'
6
+ require 'rspec/core/rake_task'
7
+
8
+ desc "Run all examples"
9
+ RSpec::Core::RakeTask.new(:spec) do |t|
10
+ t.ruby_opts = %w[-w]
11
+ t.rspec_opts = %w[--color]
6
12
  end
7
13
 
8
14
  desc "Default: Run specs"
File without changes
@@ -0,0 +1,89 @@
1
+ #!/bin/env ruby
2
+
3
+ $:.unshift File.dirname(__FILE__) + '/../lib'
4
+ require 'terminal-table/import'
5
+
6
+ Terminal::Table::Style.defaults = { :border => :unicode_round }
7
+ # Terminal::Table::UnicodeThickEdgeBorder.new()
8
+
9
+ puts
10
+ puts table(['a', 'b'], [1, 2], [3, 4])
11
+
12
+ puts
13
+ puts table(['name', 'content'], ['ftp.example.com', '1.1.1.1'], ['www.example.com', '|lalalala|lalala|'])
14
+
15
+ puts
16
+ t = table ['a', 'b']
17
+ t.style = {:padding_left => 2, :width => 80}
18
+ t << [1, 2]
19
+ t << [3, 4]
20
+ t << :separator
21
+ t << [4, 6]
22
+ puts t
23
+
24
+ puts
25
+ user_table = table do |v|
26
+ v.title = "Contact Information"
27
+ v.headings = 'First Name', 'Last Name', 'Email'
28
+ v << %w( TJ Holowaychuk tj@vision-media.ca )
29
+ v << %w( Bob Someone bob@vision-media.ca )
30
+ v << %w( Joe Whatever bob@vision-media.ca )
31
+ end
32
+ puts user_table
33
+
34
+ puts
35
+ user_table = table do |v|
36
+ v.style.width = 80
37
+ v.headings = 'First Name', 'Last Name', 'Email'
38
+ v << %w( TJ Holowaychuk tj@vision-media.ca )
39
+ v << %w( Bob Someone bob@vision-media.ca )
40
+ v << %w( Joe Whatever bob@vision-media.ca )
41
+ end
42
+ puts user_table
43
+
44
+ puts
45
+ user_table = table do
46
+ self.headings = 'First Name', 'Last Name', 'Email'
47
+ add_row ['TJ', 'Holowaychuk', 'tj@vision-media.ca']
48
+ add_row ['Bob', 'Someone', 'bob@vision-media.ca']
49
+ add_row ['Joe', 'Whatever', 'joe@vision-media.ca']
50
+ add_separator
51
+ add_row ['Total', { :value => '3', :colspan => 2, :alignment => :right }]
52
+ align_column 1, :center
53
+ end
54
+ puts user_table
55
+
56
+ puts
57
+ user_table = table do
58
+ self.headings = ['First Name', 'Last Name', {:value => 'Phones', :colspan => 2, :alignment => :center}]
59
+ #add_row ['Bob', 'Someone', '123', '456']
60
+ add_row [{:value => "Bob Someone", :colspan => 3, :alignment => :center}, '123456']
61
+ add_row :separator
62
+ add_row ['TJ', 'Holowaychuk', {:value => "No phones\navaiable", :colspan => 2, :alignment => :center}]
63
+ add_row :separator
64
+ add_row ['Joe', 'Whatever', '4324', '343242']
65
+ end
66
+ puts user_table
67
+
68
+ rows = []
69
+ rows << ['Lines', 100]
70
+ rows << ['Comments', 20]
71
+ rows << ['Ruby', 70]
72
+ rows << ['JavaScript', 30]
73
+ puts table([nil, 'Lines'], *rows)
74
+
75
+ rows = []
76
+ rows << ['Lines', 100]
77
+ rows << ['Comments', 20]
78
+ rows << ['Ruby', 70]
79
+ rows << ['JavaScript', 30]
80
+ puts table(nil, *rows)
81
+
82
+ rows = []
83
+ rows << ['Lines', 100]
84
+ rows << ['Comments', 20]
85
+ rows << ['Ruby', 70]
86
+ rows << ['JavaScript', 30]
87
+ table = table([{ :value => 'Stats', :colspan => 2, :alignment => :center }], *rows)
88
+ table.align_column 1, :right
89
+ puts table
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Methods to suppress left/right borders using border_left & border_right
4
+
5
+ require_relative "../lib/terminal-table"
6
+ table = Terminal::Table.new do |t|
7
+ t.headings = ['id', 'name']
8
+ t.rows = [[1, 'One'], [2, 'Two'], [3, 'Three']]
9
+ t.style = { :border_left => false, :border_top => false, :border_bottom => false }
10
+ end
11
+
12
+ puts table
13
+ puts
14
+
15
+ # no right
16
+ table.style = {:border_right => false }
17
+ puts table
18
+ puts
19
+
20
+ # no right
21
+ table.style = {:border_left => true }
22
+ puts table
23
+ puts
24
+
25
+ table.style.border = Terminal::Table::UnicodeBorder.new
26
+ puts table
27
+
28
+
29
+ table.style = {:border_right => false, :border_left => true }
30
+ puts table
31
+
32
+ table.style = {:border_right => true, :border_left => false }
33
+ puts table
34
+
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative "../lib/terminal-table"
3
+ puts Terminal::Table.new(headings: ['heading A', 'heading B'], rows: [['a', 'b'], ['a', 'b']], style: {border: Terminal::Table::MarkdownBorder.new})
4
+
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env ruby
2
+ require 'colorize'
3
+ require_relative '../lib/terminal-table.rb'
4
+
5
+ original_sample_data = [
6
+ ["Sep 2016", 33, [-38, -53.52], 46, [-25, -35.21]],
7
+ ["Oct 2016", 35, [2, 6.06], 50, [4, 8.69]]
8
+ ]
9
+
10
+ table = Terminal::Table.new headings: ["Month".cyan,"Monthly IT".cyan,"IT Difference OPM".cyan,
11
+ "Monthly OOT".cyan,"OOT Difference OPM".cyan], rows: original_sample_data
12
+
13
+ table.style = { padding_left: 2, padding_right: 2, border_x: "-".blue, border_y: "|".blue, border_i: "+".blue }
14
+
15
+ puts table
16
+
17
+ puts ""
18
+ puts "^ good table"
19
+ puts "v wonky table"
20
+ puts ""
21
+
22
+ split_column_sample_data = [
23
+ ["Sep 2016", 33, -38, -53.52, 46, -25, -35.21],
24
+ ["Oct 2016", 35, 2, 6.06, 50, 4, 8.69]
25
+ ]
26
+
27
+ table = Terminal::Table.new headings: ["Month".cyan,"Monthly IT".cyan,
28
+ {value: "IT Difference OPM".cyan, colspan: 2}, "Monthly OOT".cyan,
29
+ {value: "OOT Difference OPM".cyan, colspan: 2}], rows: split_column_sample_data
30
+
31
+ table.style = { padding_left: 2, padding_right: 2, border_x: "-".blue, border_y: "|".blue, border_i: "+".blue }
32
+
33
+ puts table
34
+
35
+
36
+ table = Terminal::Table.new headings: ["Month","Monthly IT",
37
+ {value: "IT Difference OPM", colspan: 2}, "Monthly OOT",
38
+ {value: "OOT Difference OPM", colspan: 2}], rows: split_column_sample_data
39
+
40
+ table.style = { padding_left: 2, padding_right: 2, border_x: "-".blue, border_y: "|".cyan, border_i: "+" }
41
+
42
+ puts table
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative "../lib/terminal-table"
3
+
4
+ #
5
+ # An example of how to manually add separators with non-default
6
+ # border_type to enable a footer row.
7
+ #
8
+ table = Terminal::Table.new do |t|
9
+ # set the style
10
+ t.style = { border: :unicode_thick_edge }
11
+
12
+ # header row
13
+ t.headings = ['fruit', 'count']
14
+
15
+ # some row data
16
+ t.add_row ['apples', 7]
17
+ t.add_row ['bananas', 19]
18
+ t.add_separator border_type: :strong
19
+ # footer row
20
+ t.add_row ['total', 26]
21
+ end
22
+
23
+ puts table.render
@@ -21,6 +21,6 @@
21
21
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
22
  #++
23
23
 
24
- %w(cell row separator style table table_helper version).each do |file|
25
- require "terminal-table/#{file}"
24
+ %w(cell row separator style table table_helper util version).each do |file|
25
+ require_relative "./terminal-table/#{file}"
26
26
  end
@@ -1,4 +1,4 @@
1
- require 'unicode/display_width'
1
+ require 'unicode/display_width/no_string_ext'
2
2
 
3
3
  module Terminal
4
4
  class Table
@@ -57,7 +57,7 @@ module Terminal
57
57
  def render(line = 0)
58
58
  left = " " * @table.style.padding_left
59
59
  right = " " * @table.style.padding_right
60
- display_width = Unicode::DisplayWidth.of(escape(lines[line]))
60
+ display_width = Unicode::DisplayWidth.of(Util::ansi_escape(lines[line]))
61
61
  render_width = lines[line].to_s.size - display_width + width
62
62
  align("#{left}#{lines[line]}#{right}", alignment, render_width + @table.cell_padding)
63
63
  end
@@ -68,7 +68,7 @@ module Terminal
68
68
  # removes all ANSI escape sequences (e.g. color)
69
69
 
70
70
  def value_for_column_width_recalc
71
- lines.map{ |s| escape(s) }.max_by{ |s| Unicode::DisplayWidth.of(s) }
71
+ lines.map{ |s| Util::ansi_escape(s) }.max_by{ |s| Unicode::DisplayWidth.of(s) }
72
72
  end
73
73
 
74
74
  ##
@@ -81,14 +81,6 @@ module Terminal
81
81
  end
82
82
  inner_width + padding
83
83
  end
84
-
85
- ##
86
- # removes all ANSI escape sequences (e.g. color)
87
- def escape(line)
88
- line.to_s.gsub(/\x1b(\[|\(|\))[;?0-9]*[0-9A-Za-z]/, '').
89
- gsub(/\x1b(\[|\(|\))[;?0-9]*[0-9A-Za-z]/, '').
90
- gsub(/(\x03|\x1a)/, '')
91
- end
92
84
  end
93
85
  end
94
86
  end
@@ -36,13 +36,31 @@ module Terminal
36
36
  end
37
37
 
38
38
  def render
39
- y = @table.style.border_y
39
+ vleft, vcenter, vright = @table.style.vertical
40
40
  (0...height).to_a.map do |line|
41
- y + cells.map do |cell|
41
+ vleft + cells.map do |cell|
42
42
  cell.render(line)
43
- end.join(y) + y
43
+ end.join(vcenter) + vright
44
44
  end.join("\n")
45
45
  end
46
+
47
+ def number_of_columns
48
+ @cells.collect(&:colspan).inject(0, &:+)
49
+ end
50
+
51
+ # used to find indices where we have table '+' crossings.
52
+ # in cases where the colspan > 1, then we will skip over some numbers
53
+ # if colspan is always 1, then the list should be incrementing by 1.
54
+ #
55
+ # skip 0 entry, because it's the left side.
56
+ # skip last entry, because it's the right side.
57
+ # we only care about "+/T" style crossings.
58
+ def crossings
59
+ idx = 0
60
+ @cells[0...-1].map { |c| idx += c.colspan }
61
+ end
62
+
46
63
  end
64
+
47
65
  end
48
66
  end
@@ -2,13 +2,65 @@ module Terminal
2
2
  class Table
3
3
  class Separator < Row
4
4
 
5
+ ##
6
+ # `prevrow`, `nextrow` contain references to adjacent rows.
7
+ #
8
+ # `border_type` is a symbol used to control which type of border is used
9
+ # on the separator (:top for top-edge, :bot for bottom-edge,
10
+ # :div for interior, and :strong for emphasized-interior)
11
+ #
12
+ # `implicit` is false for user-added separators, and true for
13
+ # implicit/auto-generated separators.
14
+
15
+ def initialize(*args, border_type: :div, implicit: false)
16
+ super
17
+ @prevrow, @nextrow = nil, nil
18
+ @border_type = border_type
19
+ @implicit = implicit
20
+ end
21
+
22
+ attr_accessor :border_type
23
+ attr_reader :implicit
24
+
5
25
  def render
6
- arr_x = (0...@table.number_of_columns).to_a.map do |i|
7
- @table.style.border_x * (@table.column_width(i) + @table.cell_padding)
26
+ left_edge, ctrflat, ctrud, right_edge, ctrdn, ctrup = @table.style.horizontal(border_type)
27
+
28
+ prev_crossings = @prevrow.respond_to?(:crossings) ? @prevrow.crossings : []
29
+ next_crossings = @nextrow.respond_to?(:crossings) ? @nextrow.crossings : []
30
+ rval = [left_edge]
31
+ numcols = @table.number_of_columns
32
+ (0...numcols).each do |idx|
33
+ rval << ctrflat * (@table.column_width(idx) + @table.cell_padding)
34
+ pcinc = prev_crossings.include?(idx+1)
35
+ ncinc = next_crossings.include?(idx+1)
36
+ border_center = if pcinc && ncinc
37
+ ctrud
38
+ elsif pcinc
39
+ ctrup
40
+ elsif ncinc
41
+ ctrdn
42
+ elsif !ctrud.empty?
43
+ # special case if the center-up-down intersection is empty
44
+ # which happens when verticals/intersections are removed. in that case
45
+ # we do not want to replace with a flat element so return empty-string in else block
46
+ ctrflat
47
+ else
48
+ ''
49
+ end
50
+ rval << border_center if idx < numcols-1
8
51
  end
9
- border_i = @table.style.border_i
10
- border_i + arr_x.join(border_i) + border_i
52
+
53
+ rval << right_edge
54
+ rval.join
55
+ end
56
+
57
+ # Save off neighboring rows, so that we can use them later in determining
58
+ # which types of table edges to use.
59
+ def save_adjacent_rows(prevrow, nextrow)
60
+ @prevrow = prevrow
61
+ @nextrow = nextrow
11
62
  end
63
+
12
64
  end
13
65
  end
14
66
  end
@@ -1,5 +1,171 @@
1
+ # coding: utf-8
2
+ require 'forwardable'
3
+
1
4
  module Terminal
2
5
  class Table
6
+
7
+ class Border
8
+
9
+ attr_accessor :data, :top, :bottom, :left, :right
10
+ def initialize
11
+ @top, @bottom, @left, @right = true, true, true, true
12
+ end
13
+ def []=(key, val)
14
+ @data[key] = val
15
+ end
16
+ def [](key)
17
+ @data[key]
18
+ end
19
+ def initialize_dup(other)
20
+ super
21
+ @data = other.data.dup
22
+ end
23
+ def remove_verticals
24
+ self.class.const_get("VERTICALS").each { |key| @data[key] = "" }
25
+ self.class.const_get("INTERSECTIONS").each { |key| @data[key] = "" }
26
+ end
27
+ def remove_horizontals
28
+ self.class.const_get("HORIZONTALS").each { |key| @data[key] = "" }
29
+ end
30
+
31
+ # If @left, return the edge else empty-string.
32
+ def maybeleft(key) ; @left ? @data[key] : '' ; end
33
+
34
+ # If @right, return the edge else empty-string.
35
+ def mayberight(key) ; @right ? @data[key] : '' ; end
36
+
37
+ end
38
+
39
+ class AsciiBorder < Border
40
+ HORIZONTALS = %i[x]
41
+ VERTICALS = %i[y]
42
+ INTERSECTIONS = %i[i]
43
+
44
+ def initialize
45
+ super
46
+ @data = { x: "-", y: "|", i: "+" }
47
+ end
48
+
49
+ # Get vertical border elements
50
+ # @return [Array] 3-element list of [left, center, right]
51
+ def vertical
52
+ [maybeleft(:y), @data[:y], mayberight(:y)] # left, center, right
53
+ end
54
+
55
+ # Get horizontal border elements
56
+ # @return [Array] a 6 element list of: [i-left, horizontal-bar, i-up/down, i-right, i-down, i-up]
57
+ def horizontal(_type)
58
+ x, i = @data[:x], @data[:i]
59
+ [maybeleft(:i), x, i, mayberight(:i), i, i]
60
+ end
61
+ end
62
+
63
+ class MarkdownBorder < AsciiBorder
64
+ def initialize
65
+ super
66
+ @top, @bottom = false, false
67
+ @data = { x: "-", y: "|", i: "|" }
68
+ end
69
+ end
70
+
71
+ class UnicodeBorder < Border
72
+
73
+ ALLOWED_SEPARATOR_BORDER_STYLES = %i[
74
+ top bot
75
+ div dash dot3 dot4
76
+ thick thick_dash thick_dot3 thick_dot4
77
+ heavy heavy_dash heavy_dot3 heavy_dot4
78
+ bold bold_dash bold_dot3 bold_dot4
79
+ double
80
+ ]
81
+
82
+ HORIZONTALS = %i[x sx ax bx nx bx_dot3 bx_dot4 bx_dash x_dot3 x_dot4 x_dash]
83
+ VERTICALS = %i[y yw ye]
84
+ INTERSECTIONS = %i[nw n ne nd
85
+ aw ai ae ad au
86
+ bw bi be bd bu
87
+ w i e dn up
88
+ sw s se su]
89
+ def initialize
90
+ super
91
+ @data = {
92
+ nil => nil,
93
+ nw: "┌", nx: "─", n: "┬", ne: "┐",
94
+ yw: "│", y: "│", ye: "│",
95
+ aw: "╞", ax: "═", ai: "╪", ae: "╡", ad: '╤', au: "╧", # double
96
+ bw: "┝", bx: "━", bi: "┿", be: "┥", bd: '┯', bu: "┷", # heavy/bold/thick
97
+ w: "├", x: "─", i: "┼", e: "┤", dn: "┬", up: "┴", # normal div
98
+ sw: "└", sx: "─", s: "┴", se: "┘",
99
+ # alternative dots/dashes
100
+ x_dot4: '┈', x_dot3: '┄', x_dash: '╌',
101
+ bx_dot4: '┉', bx_dot3: '┅', bx_dash: '╍',
102
+ }
103
+ end
104
+ # Get vertical border elements
105
+ # @return [Array] 3-element list of [left, center, right]
106
+ def vertical
107
+ [maybeleft(:yw), @data[:y], mayberight(:ye)]
108
+ end
109
+
110
+ # Get horizontal border elements
111
+ # @return [Array] a 6 element list of: [i-left, horizontal-bar, i-up/down, i-right, i-down, i-up]
112
+ def horizontal(type)
113
+ raise ArgumentError, "Border type is #{type.inspect}, must be one of #{ALLOWED_SEPARATOR_BORDER_STYLES.inspect}" unless ALLOWED_SEPARATOR_BORDER_STYLES.include?(type)
114
+ lookup = case type
115
+ when :top
116
+ [:nw, :nx, :n, :ne, :n, nil]
117
+ when :bot
118
+ [:sw, :sx, :s, :se, nil, :s]
119
+ when :double
120
+ # typically used for the separator below the heading row or above a footer row)
121
+ [:aw, :ax, :ai, :ae, :ad, :au]
122
+ when :thick, :thick_dash, :thick_dot3, :thick_dot4,
123
+ :heavy, :heavy_dash, :heavy_dot3, :heavy_dot4,
124
+ :bold, :bold_dash, :bold_dot3, :bold_dot4
125
+ # alternate thick/bold border
126
+ xref = type.to_s.sub(/^(thick|heavy|bold)/,'bx').to_sym
127
+ [:bw, xref, :bi, :be, :bd, :bu]
128
+ when :dash, :dot3, :dot4
129
+ # alternate thin dividers
130
+ xref = "x_#{type}".to_sym
131
+ [:w, xref, :i, :e, :dn, :up]
132
+ else # :div (center, non-emphasized)
133
+ [:w, :x, :i, :e, :dn, :up]
134
+ end
135
+ rval = lookup.map { |key| @data.fetch(key) }
136
+ rval[0] = '' unless @left
137
+ rval[3] = '' unless @right
138
+ rval
139
+ end
140
+ end
141
+
142
+ # Unicode Border With rounded edges
143
+ class UnicodeRoundBorder < UnicodeBorder
144
+ def initialize
145
+ super
146
+ @data.merge!({nw: '╭', ne: '╮', sw: '╰', se: '╯'})
147
+ end
148
+ end
149
+
150
+ # Unicode Border with thick outer edges
151
+ class UnicodeThickEdgeBorder < UnicodeBorder
152
+ def initialize
153
+ super
154
+ @data = {
155
+ nil => nil,
156
+ nw: "┏", nx: "━", n: "┯", ne: "┓", nd: nil,
157
+ yw: "┃", y: "│", ye: "┃",
158
+ aw: "┣", ax: "═", ai: "╪", ae: "┫", ad: '╤', au: "╧", # double
159
+ bw: "┣", bx: "━", bi: "┿", be: "┫", bd: '┯', bu: "┷", # heavy/bold/thick
160
+ w: "┠", x: "─", i: "┼", e: "┨", dn: "┬", up: "┴", # normal div
161
+ sw: "┗", sx: "━", s: "┷", se: "┛", su: nil,
162
+ # alternative dots/dashes
163
+ x_dot4: '┈', x_dot3: '┄', x_dash: '╌',
164
+ bx_dot4: '┉', bx_dot3: '┅', bx_dash: '╍',
165
+ }
166
+ end
167
+ end
168
+
3
169
  # A Style object holds all the formatting information for a Table object
4
170
  #
5
171
  # To create a table with a certain style, use either the constructor
@@ -22,17 +188,51 @@ module Terminal
22
188
  # Terminal::Table::Style.defaults = {:width => 80}
23
189
  #
24
190
  class Style
191
+ extend Forwardable
192
+ def_delegators :@border, :vertical, :horizontal, :remove_verticals, :remove_horizontals
193
+
25
194
  @@defaults = {
26
- :border_x => "-", :border_y => "|", :border_i => "+",
195
+ :border => AsciiBorder.new,
27
196
  :padding_left => 1, :padding_right => 1,
28
197
  :margin_left => '',
29
198
  :width => nil, :alignment => nil,
30
- :all_separators => false
199
+ :all_separators => false,
31
200
  }
32
201
 
33
- attr_accessor :border_x
34
- attr_accessor :border_y
35
- attr_accessor :border_i
202
+ ## settors/gettor for legacy ascii borders
203
+ def border_x=(val) ; @border[:x] = val ; end
204
+ def border_y=(val) ; @border[:y] = val ; end
205
+ def border_i=(val) ; @border[:i] = val ; end
206
+ def border_y ; @border[:y] ; end
207
+ def border_y_width ; Util::ansi_escape(@border[:y]).length ; end
208
+
209
+ # Accessor for instance of Border
210
+ attr_reader :border
211
+ def border=(val)
212
+ if val.is_a? Symbol
213
+ # convert symbol name like :foo_bar to get class FooBarBorder
214
+ klass_str = val.to_s.split('_').collect(&:capitalize).join + "Border"
215
+ begin
216
+ klass = Terminal::Table::const_get(klass_str)
217
+ @border = klass.new
218
+ rescue NameError
219
+ raise "Cannot lookup class Terminal::Table::#{klass_str} from symbol #{val.inspect}"
220
+ end
221
+ else
222
+ @border = val
223
+ end
224
+ end
225
+
226
+ def border_top=(val) ; @border.top = val ; end
227
+ def border_bottom=(val) ; @border.bottom = val ; end
228
+ def border_left=(val) ; @border.left = val ; end
229
+ def border_right=(val) ; @border.right = val ; end
230
+
231
+ def border_top ; @border.top ; end
232
+ def border_bottom ; @border.bottom ; end
233
+ def border_left ; @border.left ; end
234
+ def border_right ; @border.right ; end
235
+
36
236
 
37
237
  attr_accessor :padding_left
38
238
  attr_accessor :padding_right
@@ -44,24 +244,41 @@ module Terminal
44
244
 
45
245
  attr_accessor :all_separators
46
246
 
47
-
247
+
48
248
  def initialize options = {}
49
249
  apply self.class.defaults.merge(options)
50
250
  end
51
251
 
52
252
  def apply options
53
- options.each { |m, v| __send__ "#{m}=", v }
253
+ options.each do |m, v|
254
+ __send__ "#{m}=", v
255
+ end
54
256
  end
55
-
257
+
56
258
  class << self
57
259
  def defaults
58
- @@defaults
260
+ klass_defaults = @@defaults.dup
261
+ # border is an object that needs to be duplicated on instantiation,
262
+ # otherwise everything will be referencing the same object-id.
263
+ klass_defaults[:border] = klass_defaults[:border].dup
264
+ klass_defaults
59
265
  end
60
-
266
+
61
267
  def defaults= options
62
268
  @@defaults = defaults.merge(options)
63
269
  end
270
+
271
+ end
272
+
273
+ def on_change attr
274
+ method_name = :"#{attr}="
275
+ old_method = method method_name
276
+ define_singleton_method(method_name) do |value|
277
+ old_method.call value
278
+ yield attr.to_sym, value
279
+ end
64
280
  end
281
+
65
282
  end
66
283
  end
67
284
  end