rudy 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/bin/rudy-ec2 ADDED
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/ruby
2
+
3
+ # Rudy -- Your friend in staging and deploying to EC2
4
+ #
5
+ # See rudy -h for usage
6
+ #
7
+
8
+ #
9
+ # No Ruby 1.9.1 support. Only 1.8.x for now :[
10
+ unless RUBY_VERSION < "1.9"
11
+ puts "Sorry! We're using the right_aws gem and it doesn't support Ruby 1.9 (md5 error)."
12
+ exit 1
13
+ else
14
+ require 'rubygems'
15
+ end
16
+
17
+ RUDY_HOME = File.join(File.dirname(__FILE__), '..')
18
+ RUDY_LIB = File.join(RUDY_HOME, 'lib')
19
+ $:.unshift RUDY_LIB # Put our local lib in first place
20
+
21
+ require 'drydock'
22
+ extend Drydock
23
+
24
+ project "Rudy" # This also runs require 'ruby'
25
+
26
+ debug :on
27
+
28
+ capture :stderr
29
+
30
+ global :A, :accesskey, String, "AWS Access Key"
31
+ global :S, :secretkey, String, "AWS Secret Access Key"
32
+ global :R, :region, String, "Connect to a specific EC2 region (default: #{Rudy::DEFAULT_REGION})"
33
+ global :z, :zone, String, "Connect to a specific EC2 zone (default: #{Rudy::DEFAULT_ZONE})"
34
+ global :q, :quiet, "Run with less output"
35
+ global :v, :verbose, "Increase verbosity of output (i.e. -v or -vv or -vvv)" do
36
+ @verbose ||= 0
37
+ @verbose += 1
38
+ end
39
+ global :V, :version, "Display version number" do
40
+ puts "Rudy version: #{Rudy::VERSION}"
41
+ exit 0
42
+ end
43
+
44
+
45
+
46
+
47
+
48
+ # ----------------------------------- AMAZON EC2 COMMANDS --------
49
+ # ------------------------------------------------------------------
50
+
51
+ usage "rudy [global options] addresses [-A address instance ID]"
52
+ desc "Manage Amazon Elastic IP addresses"
53
+ argv :address, :instanceid
54
+ action :A, :associate, "Associate an address to a running instance"
55
+ command :addresses => Rudy::Command::Addresses
56
+ command_alias :addresses, :address
57
+
58
+
59
+ usage "rudy images [-C -i name [-b bucket -a account]] [-D AMI-ID]"
60
+ desc "Manage EC2 Machine Images (AMIs)"
61
+ option :a, :account, String, "Your Amazon Account Number"
62
+ option :i, :image_name, String, "The name of the image"
63
+ option :b, :bucket_name, String, "The name of the bucket that will store the image"
64
+ action :C, :create, "Create an image"
65
+ #action :P, :prepare, "Prepare a running instance to be used as an image"
66
+ action :D, :destroy, "Deregister an image (currently _does not_ remove images files from S3)"
67
+ argv :ami
68
+ command :images => Rudy::Command::Images
69
+ command_alias :images, :image
70
+
71
+
72
+ desc "Manage EC2 Volumes"
73
+ action :D, :destroy, "Destroy a volume"
74
+ argv :vol
75
+ command :volumes => Rudy::Command::Volumes
76
+ command_alias :volumes, :volume
77
+
78
+
79
+ usage "rudy [global options] instances [-D] [-S -i image ID] [instance ID OR group name]"
80
+ desc "Manage EC2 Instances"
81
+ option :all, "Display all instances"
82
+ option :a, :address, String, "Amazon elastic IP"
83
+ option :i, :image, String, "Amazon machine image ID (ami)"
84
+ #option :v, :volume, String, "Amazon volume ID"
85
+ action :D, :destroy, "Destroy the given instance IDs. All data will be lost!"
86
+ #action :S, :start, "Start an instance"
87
+ #action :R, :restart, "Restart an instance"
88
+ argv :filter
89
+ command :instances => Rudy::Command::Instances
90
+ command_alias :instances, :instance
91
+
92
+
93
+ usage "rudy [global options] groups [-C] [-a IP addresses] [-p ports] [group name]"
94
+ desc "Manage EC2 Security Groups"
95
+ option :all, "Display all security groups"
96
+ option :r, :protocols, Array, "Comma-separated list of protocols. One of: tcp (default), udp, icmp"
97
+ option :p, :ports, Array, "List of comma-separated ports to authorize (default: 22,80,443)"
98
+ option :a, :addresses, Array, "List of comma-separated IP addresses to authorize (default: your external IP)"
99
+ action :C, :create, "Create a security group"
100
+ action :D, :destroy, "Destroy a security group"
101
+ action :M, :modify, "Modify a security group"
102
+ argv :group
103
+ command :groups => Rudy::Command::Groups
104
+ command_alias :groups, :group
105
+
106
+
107
+
data/lib/console.rb CHANGED
@@ -1,341 +1,385 @@
1
-
1
+ #---
2
2
  # Adapted from: http://github.com/oneup/ruby-console/tree/tput
3
+ # See: http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x405.html
4
+ # See: man terminfo
5
+ #+++
3
6
 
4
- if __FILE__ == $0
5
- puts "This is a module, don't run it from the command line."
6
- exit
7
- end
8
7
 
9
8
  class String
10
- def color(*arg) # colorize a string
11
- Console.color(arg) +
9
+
10
+ # +col+, +bgcol+, and +attribute+ are symbols corresponding
11
+ # to Console::COLOURS, Console::BGCOLOURS, and Console::ATTRIBUTES.
12
+ # Returns the string in the format attributes + string + defaults.
13
+ #
14
+ # "MONKEY_JUNK".colour(:blue, :white, :blink) # => "\e[34;47;5mMONKEY_JUNK\e[39;49;0m"
15
+ #
16
+ def colour(col, bgcol = nil, attribute = nil)
17
+ Console.style(col, bgcol, attribute) +
12
18
  self +
13
- Console.color(:default)
19
+ Console.style(:default, :default, :default)
14
20
  end
15
- def printAt(*arg)
16
- row = 0
17
- col = 0
18
- if not arg.is_a?(Array)
19
- arg = [arg]
20
- end
21
- if arg.length > 0
22
- row = arg[0]
23
- end
24
- if arg.length > 1
25
- col = arg[1]
26
- end
27
- Cursor.save
28
- Cursor.position = row, col
29
- print self
30
- Cursor.restore
21
+ alias :color :colour
22
+
23
+ # See colour
24
+ def bgcolour(bgcol = :default)
25
+ Console.style(nil, bgcol, nil) +
26
+ self +
27
+ Console.style(nil, :default, nil)
31
28
  end
32
- end
33
-
34
- class Console
35
- def self.clear
36
- system "tput clear"
29
+ alias :bgcolor :bgcolour
30
+
31
+ # See colour
32
+ def att(a = :default)
33
+ Console.style(nil, nil, a) +
34
+ self +
35
+ Console.style(nil, nil, :default)
37
36
  end
38
-
39
- def self.reset
40
- system "tput reset"
37
+
38
+
39
+ # Print the string at +x+ +y+. When +minus+ is any true value
40
+ # the length of the string is subtracted from the value of x
41
+ # before printing.
42
+ def print_at(x=nil, y=nil, minus=false)
43
+ args = {:minus=>minus}
44
+ args[:x] &&= x
45
+ args[:y] &&= y
46
+ Console.print_at(self, args)
41
47
  end
42
48
 
43
- def self.color(*arg) # colorize a string
44
- if arg[0].is_a?(Array)
45
- arg = arg[0]
46
- end
47
- if arg.length == 0
48
- arg = :default, :red, :bg_default
49
- end
50
- attribute = { # mapper for the attributes
51
- :normal => 0,
52
- :bright => 1,
53
- :dim => 2,
54
- :underscore => 4,
55
- :blink => 5,
56
- :reverse => 7,
57
- :hidden => 8,
58
- :default => 0
59
- }
60
- fg_color = { # mapper for the foreground color
61
- :black => 30,
62
- :red => 31,
63
- :green => 32,
64
- :yellow => 33,
65
- :blue => 34,
66
- :magenta => 35,
67
- :cyan => 36,
68
- :white => 37,
69
- :default => 39
70
- }
71
- bg_color = { # mapper for the background color
72
- :bg_black => 40,
73
- :bg_red => 41,
74
- :bg_green => 42,
75
- :bg_yellow => 43,
76
- :bg_blue => 44,
77
- :bg_magenta => 45,
78
- :bg_cyan => 46,
79
- :bg_white => 47,
80
- :bg_default => 49
81
- }
82
- if arg.length > 0 # turn symbols into numbers
83
- arg[0] = attribute[arg[0]] # attributes
84
- end
85
- if arg.length > 1
86
- arg[1] = fg_color[arg[1]] # foreground color
87
- end
88
- if arg.length > 2
89
- arg[2] = bg_color[arg[2]] # background color
90
- end
91
- "\e[#{arg.join(";")}m" # magic ansi escape sequence
49
+ # Returns the string with escape code attributes removed.
50
+ # NOTE: The non-printable attributes count towards the string size.
51
+ # You can use this method to get the "visible" size:
52
+ #
53
+ # "\e[34;47;5mMONKEY_JUNK\e[39;49;0m".noatt.size # => 11
54
+ # "\e[34;47;5mMONKEY_JUNK\e[39;49;0m".size # => 31
55
+ #
56
+ def noatt
57
+ gsub(/\e\[[\d;]*m/, '')
92
58
  end
59
+ end
93
60
 
94
- def self.color=(arg)
95
- if not arg.is_a?(Array)
96
- arg = [arg]
97
- end
98
- if arg.length == 0
99
- arg = :default, :red, :bg_default
100
- end
101
- print self.color(arg)
102
- end
61
+ class Object
103
62
 
104
- def self.width
105
- `tput cols`.chomp.to_i
63
+ # Executes tput +capnam+ with +args+. Returns true if tcap gives
64
+ # 0 exit status and false otherwise.
65
+ #
66
+ # tput :cup, 1, 4
67
+ # $ tput cup 1 4
68
+ #
69
+ def tput(capnam, *args)
70
+ system("tput #{capnam} #{args.flatten.join(' ')}")
106
71
  end
107
-
108
- def self.height
109
- `tput lines`.chomp.to_i
72
+
73
+ # Executes tput +capnam+ with +args+. Returns the output of tput.
74
+ #
75
+ # tput_val :cols # => 16
76
+ # $ tput cols # => 16
77
+ #
78
+ def tput_val(capnam, *args)
79
+ `tput #{capnam} #{args.flatten.join(' ')}`.chomp
110
80
  end
111
81
  end
112
82
 
113
- class Cursor
114
- def self.position
115
- row = ""
116
- col = ""
117
- c = ""
118
-
119
- termsettings = `stty -g`
120
- system("stty raw -echo")
121
- print "\e[6n"
122
- while (c = STDIN.getc.chr) != ";"
123
- if c == "\e" or c == "["
124
- next
125
- else
126
- row += c
127
- end
128
- end
129
- while (c = STDIN.getc.chr) != "R"
130
- col += c
131
- end
132
- system("stty #{termsettings}")
133
-
134
- [row, col]
135
- end
136
83
 
137
- def self.position=(arg)
138
- row = 0
139
- col = 0
140
- if not arg.is_a?(Array)
141
- arg = [arg]
142
- end
143
- if arg.length > 0
144
- row = arg[0]
145
- end
146
- if arg.length > 1
147
- col = arg[1]
148
- end
149
- system "tput cup #{row} #{col}"
150
- end
151
84
 
152
- def self.up(*arg)
153
- if arg.length == 0
154
- arg[0] == 1
85
+ module Console
86
+ extend self
87
+ require 'timeout'
88
+ require 'thread'
89
+
90
+ # ANSI escape sequence numbers for text attributes
91
+ ATTRIBUTES = {
92
+ :normal => 0,
93
+ :bright => 1,
94
+ :dim => 2,
95
+ :underline => 4,
96
+ :blink => 5,
97
+ :reverse => 7,
98
+ :hidden => 8,
99
+ :default => 0,
100
+ }.freeze unless defined? ATTRIBUTES
101
+
102
+ # ANSI escape sequence numbers for text colours
103
+ COLOURS = {
104
+ :black => 30,
105
+ :red => 31,
106
+ :green => 32,
107
+ :yellow => 33,
108
+ :blue => 34,
109
+ :magenta => 35,
110
+ :cyan => 36,
111
+ :white => 37,
112
+ :default => 39,
113
+ :random => 30 + rand(10).to_i
114
+ }.freeze unless defined? COLOURS
115
+
116
+ # ANSI escape sequence numbers for background colours
117
+ BGCOLOURS = {
118
+ :black => 40,
119
+ :red => 41,
120
+ :green => 42,
121
+ :yellow => 43,
122
+ :blue => 44,
123
+ :magenta => 45,
124
+ :cyan => 46,
125
+ :white => 47,
126
+ :default => 49,
127
+ :random => 40 + rand(10).to_i
128
+ }.freeze unless defined? BGCOLOURS
129
+
130
+
131
+ def print_left(str, props={})
132
+ props[:x] ||= 0
133
+ props[:y] ||= Cursor.y
134
+ # print_at("x:#{props[:x]} y:#{props[:y]}", {:x => 0, :y => 10})
135
+ print_at(str, props)
136
+ end
137
+ def print_right(str, props={})
138
+ props[:x] ||= width
139
+ props[:y] ||= Cursor.y
140
+ props[:minus] = true unless props.has_key?(:minus)
141
+ print_at(str, props)
142
+ end
143
+ def print_spaced(*args)
144
+ props = (args.last.is_a? Hash) ? args.pop : {}
145
+ props[:y] = Cursor.y
146
+ chunk_width = (width / args.flatten.size).to_i
147
+ chunk_at = 0
148
+ args.each do |chunk|
149
+ props[:x] = chunk_at
150
+ print_at(chunk.to_s[0, chunk_width], props)
151
+ chunk_at += chunk_width
155
152
  end
156
- print "\e[#{arg[0]}A"
153
+ puts
154
+ end
155
+ def print_center(str, props={})
156
+ props[:x] = ((width - str.noatt.length) / 2).to_i-1
157
+ props[:y] ||= height
158
+ print_at(str, props)
159
+ end
160
+ def print_at(str, props={})
161
+ print_at_lamb = lambda {
162
+ #props[:x] ||= 0
163
+ #props[:y] ||= 0
164
+ props[:minus] = false unless props.has_key?(:minus)
165
+ props[:x] = props[:x]-str.noatt.size if props[:x] && props[:minus] # Subtract the str length from the position
166
+ Cursor.save
167
+ Cursor.move = 0
168
+ print str
169
+ Cursor.restore
170
+ }
171
+ RUBY_VERSION =~ /1.9/ ? Thread.exclusive(&print_at_lamb) : print_at_lamb.call
157
172
  end
158
-
159
- def self.down(*arg)
160
- if arg.length == 0
161
- arg[0] == 1
162
- end
163
- print "\e[#{arg[0]}B"
173
+
174
+ def self.style(col, bgcol=nil, att=nil)
175
+ valdor = []
176
+ valdor << COLOURS[col] if COLOURS.has_key?(col)
177
+ valdor << BGCOLOURS[bgcol] if BGCOLOURS.has_key?(bgcol)
178
+ valdor << ATTRIBUTES[att] if ATTRIBUTES.has_key?(att)
179
+ "\e[#{valdor.join(";")}m" # => \e[8;34;42m
164
180
  end
165
-
166
- def self.right(*arg)
167
- if arg.length == 0
168
- arg[0] == 1
169
- end
170
- system "tput cuf #{arg[0]}"
181
+
182
+ def self.clear
183
+ tput :clear
171
184
  end
172
185
 
173
- def self.left(*arg)
174
- if arg.length == 0
175
- arg[0] == 1
176
- end
177
- system "tput cub #{arg[0]}"
186
+ def reset
187
+ tput :reset
178
188
  end
179
-
180
- def self.save
181
- system "tput sc"
189
+
190
+ def width
191
+ tput_val(:cols).to_i
182
192
  end
183
193
 
184
- def self.restore
185
- system "tput rc"
194
+ def height
195
+ tput_val(:lines).to_i
186
196
  end
187
197
  end
188
198
 
189
- class Win
190
- def initialize
191
- @row = 1
192
- @col = 1
193
- @width = 10
194
- @height = 5
195
- @text = ""
196
- @border = :single
197
- @fg = :default
198
- @bg = :bg_default
199
- @bordercolor = :default
199
+ module Cursor
200
+ extend self
201
+
202
+ # Returns [x,y] for the current cursor position.
203
+ def position
204
+ yx = [0,0]
205
+
206
+ position_lamb = lambda {
207
+ begin
208
+ # NOTE: Can we get cursor position from tput?
209
+ termsettings = `stty -g`
210
+
211
+ # DEBUGGING: The following code works in Ruby 1.9 but not 1.8.
212
+
213
+ system("stty raw -echo")
214
+ print "\e[6n" # Forces output of: \e[49;1R (\e is not printable)
215
+ c = ''
216
+ (pos ||= '') << c while (c = STDIN.getc) != 'R'# NOTE: There must be a better way!
217
+ yx = pos.scan(/(\d+);(\d+)/).flatten
218
+ yx[0] = yx[0].to_i - 1 # It returns 1 for the first column, but we want 0
219
+ yx[1] = yx[1].to_i - 1
220
+ ensure
221
+ system("stty #{termsettings}") # Get out of raw mode
222
+ end
223
+ }
224
+
225
+ RUBY_VERSION =~ /1.9/ ? Thread.exclusive(&position_lamb) : position_lamb.call
226
+ yx.reverse
200
227
  end
201
228
 
202
- def width=(width)
203
- @width = width
204
- end
229
+ def x; position[0]; end
230
+ def y; position[1]; end
205
231
 
206
- def width
207
- @width
232
+ def move=(*args)
233
+ x,y = *args.flatten
234
+ tput(:cup, y, x) # "tput cup" takes y before x
208
235
  end
209
236
 
210
- def height=(height)
211
- @height = height
237
+ def up(n=1)
238
+ tput :cuu, n
212
239
  end
213
240
 
214
- def height
215
- @height
241
+ def down(n=1)
242
+ tput :cud, n
216
243
  end
217
244
 
218
- def text=(text)
219
- @text = text
245
+ def right(x=1)
246
+ tput :cuf, x
220
247
  end
221
248
 
222
- def text
223
- @text
249
+ def left(x=1)
250
+ tput :cub, x
224
251
  end
225
-
226
- def border=(arg)
227
- if not arg.is_a?(Array)
228
- arg = [arg]
229
- end
230
- if arg.length > 0
231
- @border = arg[0]
232
- end
233
- if arg.length > 1
234
- @bordercolor = arg[1]
235
- end
252
+
253
+ def line(n=1)
254
+ tput :il, n
236
255
  end
237
-
238
- def border
239
- [@border, @bordercolor]
256
+
257
+ def save
258
+ tput :sc
240
259
  end
241
260
 
242
- def position=(arg)
243
- if not arg.is_a?(Array)
244
- arg = [arg]
245
- end
246
- if arg.length > 0
247
- @row = arg[0]
248
- end
249
- if arg.length > 1
250
- @col = arg[1]
251
- end
261
+ def restore
262
+ tput :rc
252
263
  end
253
-
254
- def position
255
- [@row, @col]
264
+
265
+ def clear_line
266
+ tput :el
256
267
  end
268
+
269
+ # TODO: replace methods with this kinda thing
270
+ #@@capnames = {
271
+ # :restore => [:rc],
272
+ # :save => [:sc],
273
+ # :clear_line => [:el],
274
+ # :line => [:il, 1, 1],
275
+ #
276
+ # :up => [:cuu, 1, 1],
277
+ # :down => [:cud, 1, 1],
278
+ # :right => [:cuf, 1, 1],
279
+ # :left => [:cub, 1, 1],
280
+ #
281
+ # :move => [:cup, 2, 0, 0]
282
+ #}
283
+ #
284
+ #@@capnames.each_pair do |meth, cap|
285
+ # module_eval <<-RUBY
286
+ # def #{meth}(*args)
287
+ # tput '#{cap[0]}'
288
+ # end
289
+ # RUBY
290
+ #end
291
+
292
+ end
257
293
 
258
- def fg=(fg)
259
- @fg = fg
294
+ class Window
295
+ attr_accessor :row, :col, :width, :height, :text, :fg, :bg
296
+ attr_reader :threads
297
+
298
+ def initialize(*args)
299
+ @row = 1
300
+ @col = 1
301
+ @width = 10
302
+ @height = 5
303
+ @text = ""
304
+ @fg = :default
305
+ @bg = :default
306
+ @threads = []
260
307
  end
261
308
 
262
- def fg
263
- @fg
309
+ def position=(x,y=nil)
310
+ @x = x
311
+ @y = y if y
264
312
  end
265
313
 
266
- def bg=(bg)
267
- @bg = bg
314
+ def position
315
+ [@row, @col]
268
316
  end
269
-
270
- def bg
271
- @bg
317
+
318
+ def self.bar(len, unit='=')
319
+ unit*len
272
320
  end
273
321
 
274
- def drawBorder
275
- single = {
276
- :upper_left => "\u250C",
277
- :upper_right => "\u2510",
278
- :lower_left => "\u2514",
279
- :lower_right => "\u2518",
280
- :horizontal => "\u2500",
281
- :vertical => "\u2502"
282
- }
283
- bold = {
284
- :upper_left => "\u250D",
285
- :upper_right => "\u2511",
286
- :lower_left => "\u2515",
287
- :lower_right => "\u2519",
288
- :horizontal => "\u2501",
289
- :vertical => "\u2503"
290
- }
291
- double = {
292
- :upper_left => "\u2554",
293
- :upper_right => "\u2557",
294
- :lower_left => "\u255A",
295
- :lower_right => "\u255D",
296
- :horizontal => "\u2550",
297
- :vertical => "\u2551"
298
- }
299
- round = {
300
- :upper_left => "\u256D",
301
- :upper_right => "\u256E",
302
- :lower_left => "\u2570",
303
- :lower_right => "\u256F",
304
- :horizontal => "\u2500",
305
- :vertical => "\u2502"
306
- }
307
- mapper = case @border
308
- when :single then single
309
- when :bold then bold
310
- when :double then double
311
- when :round then round
312
- else single
313
- end
314
- (mapper[:upper_left] + mapper[:horizontal] * (@width - 2) + mapper[:upper_right]).color(:normal, @bordercolor, @bg).printAt @row, @col
315
- (mapper[:lower_left] + mapper[:horizontal] * (@width - 2) + mapper[:lower_right]).color(:normal, @bordercolor, @bg).printAt @row + @height - 1, @col
316
- 0.upto(@height - 3) do |i|
317
- (mapper[:vertical] + " " * (@width - 2) + mapper[:vertical]).color(:normal, @bordercolor, @bg).printAt @row + i + 1, @col
322
+
323
+ # Execute the given block every +n+ seconds in a separate thread.
324
+ # The lower limit for +n+ is 1 second.
325
+ # Returns a Thread object.
326
+ def every_n_seconds(n)
327
+ #n = 1 if n < 1
328
+ thread = Thread.new do
329
+
330
+ begin
331
+ while true
332
+ before = Time.now
333
+ yield
334
+ interval = n - (Time.now - before)
335
+ sleep(interval) if interval > 0
336
+ end
337
+ rescue Interrupt
338
+ break
339
+ ensure
340
+ thread
341
+ end
318
342
  end
319
343
  end
320
-
321
- def draw
322
- text = @text.split("\n")
323
- 0.upto(text.length - 1) do |i|
324
- if @border != :none
325
- text[i] = text[i][0, @width - 2]
326
- else
327
- text[i] = text[i][0, @width]
328
- end
344
+
345
+ # Print text to the screen via +type+ every +refresh+ seconds.
346
+ # Print the return value of the block to the screen using the
347
+ # print_+type+ method. +refresh+ is number of seconds to wait
348
+ # +props+ is the hash sent to print_+type+.
349
+ # Returns a Thread object.
350
+ #
351
+ # # Print the time in the upper right corner every second
352
+ # thread1 = Console.static(:right, 1, {:y => 0}) do
353
+ # Time.now.utc.strftime("%Y-%m-%d %H:%M:%S").colour(:blue, :white, :underline)
354
+ # end
355
+ #
356
+ def static(type, refresh=2, props={}, &b)
357
+ meth = "print_#{type}"
358
+ raise "#{meth} is not supported" unless Console.respond_to?(meth)
359
+
360
+ refresh ||= 0
361
+ refreh = refresh.to_s.to_i
362
+
363
+ thread = every_n_seconds(refresh) do
364
+ Console.send(meth, b.call, props.clone)
329
365
  end
330
- if @border == :none
331
- 0.upto((text.length > @height ? @height : text.length) - 1) do |i|
332
- text[i].color(:normal, @fg, @bg).printAt @row + i , @col
366
+
367
+ @threads << thread
368
+
369
+ thread
370
+ end
371
+
372
+ def join_threads
373
+ begin
374
+ @threads.each do |t|
375
+ t.join
333
376
  end
334
- else
335
- drawBorder
336
- 0.upto(@text.length > @height - 3 ? @height - 3 : text.length) do |i|
337
- text[i].to_s.color(:normal, @fg, @bg).printAt @row + i + 1, @col + 1
377
+ rescue Interrupt
378
+ ensure
379
+ @threads.each do |t|
380
+ t.kill
338
381
  end
339
382
  end
340
383
  end
384
+
341
385
  end