monocle-print 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,491 @@
1
+ #!/usr/bin/ruby
2
+ # encoding: utf-8
3
+
4
+ require 'delegate'
5
+ autoload :StringIO, 'stringio' unless defined?( StringIO )
6
+
7
+ module MonoclePrint
8
+ class OutputDevice < DelegateClass( IO )
9
+ include MonoclePrint
10
+ include TerminalEscapes
11
+
12
+ def self.open( path, options = {} )
13
+ File.open( path, options.fetch( :mode, 'w' ) ) do | file |
14
+ return( yield( new( file, options ) ) )
15
+ end
16
+ end
17
+
18
+ def self.buffer( options = {} )
19
+ case options
20
+ when Numeric then options = { :width => options }
21
+ when nil then options = {}
22
+ end
23
+
24
+ buffer = StringIO.new( '', options.fetch( :mode, 'w' ) )
25
+ out = new( buffer, options )
26
+ if block_given?
27
+ begin
28
+ yield( out )
29
+ return( out.string )
30
+ ensure
31
+ out.close
32
+ end
33
+ else
34
+ return out
35
+ end
36
+ end
37
+
38
+ def self.stdout( options = {} )
39
+ device = new( $stdout, options )
40
+ block_given? ? yield( device ) : device
41
+ end
42
+
43
+ DEFAULT_SIZE = Pair.new( 80, 22 ).freeze
44
+ IO_PRINT_METHODS = %w( puts print printf putc )
45
+ SIZE_IOCTL = 0x5413
46
+ SIZE_STRUCT = [ 0, 0, 0, 0 ].pack( "SSSS" ).freeze
47
+
48
+ private :reopen
49
+ attr_reader :device
50
+ attr_accessor :style
51
+
52
+ def initialize( output, options = {} )
53
+ @device =
54
+ case output
55
+ when String then File.open( output, options.fetch( :mode, 'w' ) )
56
+ when IO then output
57
+ else
58
+ if IO_PRINT_METHODS.all? { | m | output.respond_to?( m ) }
59
+ output
60
+ else
61
+ msg = "%s requires an IO-like object, but was given: %p" % [ self.class, output ]
62
+ raise( ArgumentError, msg )
63
+ end
64
+ end
65
+ super( @device )
66
+
67
+ @cursor = Pair.new( 0, 0 )
68
+ @background_stack = []
69
+ @foreground_stack = []
70
+ @background = @foreground = nil
71
+ @tab_width = options.fetch( :tab_width, 8 )
72
+
73
+ @newline = options.fetch( :newline, $/ )
74
+ @use_color = options.fetch( :use_color, tty? )
75
+
76
+ @margin = Pair.new( 0, 0 )
77
+ case margin = options.fetch( :margin, 0 )
78
+ when Numeric then @margin.left = @margin.right = margin.to_i
79
+ else
80
+ @margin.left = margin[ :left ].to_i
81
+ @margin.right = margin[ :right ].to_i
82
+ end
83
+
84
+ @tabs = {}
85
+ @screen_size = nil
86
+ @forced_width = options[ :width ]
87
+ @forced_height = options[ :height ]
88
+ @alignment = options.fetch( :alignment, :left )
89
+ @style = Style( options[ :style ] )
90
+ end
91
+
92
+ def foreground( color = nil )
93
+ color or return( @foreground_stack.last )
94
+ begin
95
+ @foreground_stack.push( color )
96
+ yield
97
+ ensure
98
+ @foreground_stack.pop
99
+ end
100
+ end
101
+
102
+ def colors( fg, bg )
103
+ foreground( fg ) { background( bg ) { yield } }
104
+ end
105
+
106
+ def background( color = nil )
107
+ color or return( @background_stack.last )
108
+ begin
109
+ @background_stack.push( color )
110
+ yield
111
+ ensure
112
+ @background_stack.pop
113
+ end
114
+ end
115
+
116
+ alias on background
117
+
118
+ for color in ANSI_COLORS.keys
119
+ class_eval( <<-END, __FILE__, __LINE__ )
120
+ def #{ color }
121
+ foreground( :#{ color } ) { yield }
122
+ end
123
+
124
+ def on_#{ color }
125
+ background( :#{ color } ) { yield }
126
+ end
127
+ END
128
+ end
129
+
130
+ def use_color?
131
+ @use_color
132
+ end
133
+
134
+ def use_color( v = nil )
135
+ v.nil? or self.use_color = v
136
+ return( @use_color )
137
+ end
138
+
139
+ def use_color= v
140
+ @use_color = ! ! v # 'not not x' casts x to either `true' or 'false'
141
+ end
142
+
143
+ def margin= n
144
+ left_margin = right_margin = Utils.at_least( n.to_i, 0 )
145
+ end
146
+
147
+ def indent( n = 0, m = 0 )
148
+ n, m = n.to_i, m.to_i
149
+ self.left_margin += n
150
+ self.right_margin += m
151
+ if block_given?
152
+ begin
153
+ yield
154
+ ensure
155
+ self.left_margin -= n
156
+ self.right_margin -= m
157
+ end
158
+ else
159
+ self
160
+ end
161
+ end
162
+
163
+ def outdent( n )
164
+ self.left_margin -= n.to_i
165
+ block_given? and
166
+ begin yield
167
+ ensure self.left_margin += n.to_i
168
+ end
169
+ end
170
+
171
+ def left_margin
172
+ @margin.left
173
+ end
174
+
175
+ def right_margin
176
+ @margin.right
177
+ end
178
+
179
+ def left_margin= n
180
+ @margin.left = n.to_i
181
+ end
182
+
183
+ def right_margin= n
184
+ @margin.right = n.to_i
185
+ end
186
+
187
+ def reset!
188
+ @fg_stack.clear
189
+ @bg_stack.clear
190
+ @margin = Pair.new( 0, 0 )
191
+ @cursor = Pair.new( 0, 0 )
192
+ @screen_size = nil
193
+ end
194
+
195
+ alias use_color? use_color
196
+
197
+ def list( *args )
198
+ List.new( *args ) do | list |
199
+ list.output = self
200
+ block_given? and yield( list )
201
+ list.render
202
+ end
203
+ return( self )
204
+ end
205
+
206
+ def table( *args )
207
+ Table.new( *args ) do | t |
208
+ block_given? and yield( t )
209
+ t.render( self )
210
+ end
211
+ end
212
+
213
+ def column_layout( *args )
214
+ ColumnLayout.new( *args ) do | t |
215
+ block_given? and yield( t )
216
+ t.render( self )
217
+ end
218
+ end
219
+
220
+ def leger( char = '<h>', w = width )
221
+ puts( @style.format( char ).tile( w ) )
222
+ end
223
+
224
+ def print( *objs )
225
+ text = Text( [ objs ].flatten!.join )
226
+ @use_color or text.bleach!
227
+ last_line = text.pop
228
+ for line in text
229
+ put!( line )
230
+ end
231
+ fill( @margin.left )
232
+ @device.print( color_code )
233
+ @cursor + last_line.width
234
+ @device.print( last_line )
235
+ self
236
+ end
237
+
238
+ def puts( *objs )
239
+ text = Text( [ objs ].flatten!.join( @newline ) )
240
+ ( text.empty? or text.last.empty? ) and text << Line( '' )
241
+ for line in text
242
+ put( line )
243
+ newline!
244
+ end
245
+ self
246
+ end
247
+
248
+ def printf( fmt, *args )
249
+ print( sprintf( fmt, *args ) )
250
+ end
251
+
252
+ def putsf( fmt, *args )
253
+ puts( sprintf( fmt, *args ) )
254
+ end
255
+
256
+ for m in %w( left right center )
257
+ class_eval( <<-END, __FILE__, __LINE__ + 1 )
258
+ def #{ m }
259
+ prior, @alignment = @alignment, :#{ m }
260
+ yield
261
+ ensure
262
+ @alignment = prior
263
+ end
264
+ END
265
+ end
266
+
267
+ def close
268
+ @device.close rescue nil
269
+ end
270
+
271
+ def height
272
+ screen_size.height
273
+ end
274
+
275
+ def full_width
276
+ screen_size.width
277
+ end
278
+
279
+ def width
280
+ screen_size.width - @margin.left - @margin.right
281
+ end
282
+
283
+ def put( str, options = nil )
284
+ if options
285
+ fill = @style.format( options.fetch( :fill, ' ' ) )
286
+ align = options.fetch( :align, @alignment )
287
+ else
288
+ fill, align = ' ', @alignment
289
+ end
290
+
291
+ str = SingleLine.new( str ).align!( align, width, fill )
292
+ code = color_code
293
+ str.gsub!( /\e\[0m/ ) { "\e[0m" << code }
294
+
295
+ fill( @margin.left )
296
+ @device.print( code )
297
+ @device.print( str )
298
+ @cursor + str.width
299
+ @device.print( clear_attr )
300
+ fill( @screen_size.width - @cursor.column )
301
+ self
302
+ end
303
+
304
+ def put!( str, options = nil )
305
+ put( str, options )
306
+ newline!
307
+ end
308
+
309
+ def return!
310
+ ~@cursor
311
+ @device.print("\r")
312
+ @device.flush
313
+ end
314
+
315
+ def fill( width, char = ' ' )
316
+ width =
317
+ case width
318
+ when Symbol, String
319
+ distance_to( width )
320
+ when Fixnum
321
+ Utils.at_least( width, 0 )
322
+ end
323
+ if width > 0
324
+ fill_str =
325
+ if char.length > 1 and ( char = SingleLine( char ) ).width > 1
326
+ char.tile( width )
327
+ else
328
+ char * width
329
+ end
330
+ @cursor.column += width
331
+ @device.print( fill_str )
332
+ end
333
+ self
334
+ end
335
+
336
+ def newline!
337
+ +@cursor
338
+ @device.print( @newline )
339
+ @device.flush
340
+ self
341
+ end
342
+
343
+ def space( n_lines = 1 )
344
+ n_lines.times do
345
+ put( '' )
346
+ newline!
347
+ end
348
+ self
349
+ end
350
+
351
+ def column
352
+ @cursor.column
353
+ end
354
+
355
+ def line
356
+ @cursor.line
357
+ end
358
+
359
+ def clear
360
+ @cursor.line = @cursor.column = 0
361
+ @device.print( set_cursor( 0, 0 ), clear_screen )
362
+ @device.flush
363
+ return( self )
364
+ end
365
+
366
+ def clear_line
367
+ @device.print( super )
368
+ @device.flush
369
+ return!
370
+ end
371
+
372
+
373
+ for m in %w( horizontal_line box_top box_bottom )
374
+ class_eval( <<-END, __FILE__, __LINE__ + 1 )
375
+ def #{ m }
376
+ put( @style.#{ m }( width ) )
377
+ newline!
378
+ end
379
+ END
380
+ end
381
+
382
+ def color_code
383
+ @use_color or return ''
384
+ code = ''
385
+ case fg = @foreground_stack.last
386
+ when Fixnum then code << xterm_color( ?f, fg )
387
+ when String, Symbol then code << ansi_color( ?f, fg )
388
+ end
389
+ case bg = @background_stack.last
390
+ when Fixnum then code << xterm_color( ?b, bg )
391
+ when String, Symbol then code << ansi_color( ?b, bg )
392
+ end
393
+ code
394
+ end
395
+
396
+ private
397
+
398
+ def abs( col )
399
+ col < 0 and col += width
400
+ Utils.bound( col, 0, width - 1 )
401
+ end
402
+
403
+
404
+ def distance_to( tab )
405
+ Utils.at_least( abs( @tabs[ tab ] ) - @cursor.columm, 0 )
406
+ end
407
+
408
+ def screen_size
409
+ @screen_size ||= begin
410
+ data = SIZE_STRUCT.dup
411
+ if @device.ioctl( SIZE_IOCTL, data ) >= 0
412
+ height, width = data.unpack( "SS" )
413
+ @forced_width and width = @forced_width
414
+ @forced_height and height = @forced_height
415
+ Pair.new(
416
+ width > 0 ? width : default_width,
417
+ height > 0 ? height : default_height
418
+ )
419
+ else
420
+ default_size
421
+ end
422
+ rescue Exception
423
+ default_size
424
+ end
425
+ end
426
+
427
+ def default_size
428
+ Pair.new( @forced_width || default_width, @forced_height || default_height )
429
+ end
430
+
431
+ def default_height
432
+ ( ENV[ 'LINES' ] || DEFAULT_SIZE.height ).to_i
433
+ end
434
+
435
+ def default_width
436
+ ( ENV[ 'COLUMNS' ] || DEFAULT_SIZE.width ).to_i
437
+ end
438
+ end
439
+
440
+ class Pager < OutputDevice
441
+ unless pager_command = ENV['PAGER']
442
+ pagers = %w( most less more )
443
+ system_path = ENV[ "PATH" ].split( File::PATH_SEPARATOR )
444
+ pager_command = pagers.find do | cmd |
445
+ system_path.find { | dir | test( ?x, File.join( dir, cmd ) ) }
446
+ end
447
+ end
448
+ PAGER_COMMAND = pager_command
449
+
450
+ def self.open( options = {} )
451
+ unless PAGER_COMMAND
452
+ message = <<-END.gsub!( /\s+/, ' ' ).strip!
453
+ unable to locate a pager program on the system's PATH or from
454
+ the environmental variable, PAGER
455
+ END
456
+ raise( IOError, message )
457
+ end
458
+
459
+ options.fetch( :use_color ) { options[ :use_color ] = true }
460
+
461
+ if block_given?
462
+ IO.popen( PAGER_COMMAND, 'w' ) do | pager |
463
+ pager = new( pager, options )
464
+ return yield( pager )
465
+ end
466
+ else
467
+ return new( IO.popen( PAGER_COMMAND, 'w' ), options )
468
+ end
469
+ end
470
+
471
+ private
472
+
473
+ def screen_size
474
+ @screen_size ||= begin
475
+ data = SIZE_STRUCT.dup
476
+ if STDOUT.ioctl( SIZE_IOCTL, data ) >= 0
477
+ height, width = data.unpack( "SS" )
478
+ Pair.new(
479
+ width > 0 ? width : default_width,
480
+ height > 0 ? height : default_height
481
+ )
482
+ else
483
+ default_size
484
+ end
485
+ rescue Exception
486
+ default_size
487
+ end
488
+ end
489
+ end
490
+
491
+ end
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/ruby
2
+ # encoding: utf-8
3
+
4
+ module MonoclePrint
5
+ module Presentation
6
+ include MonoclePrint
7
+ ALIGNMENTS = [ :left, :right, :center ]
8
+
9
+ def self.included( klass )
10
+ super
11
+ klass.extend( ClassMethods )
12
+ end
13
+
14
+ attr_accessor :owner
15
+ protected :owner=
16
+
17
+ for attr in %w( margin padding border )
18
+ class_eval( <<-END, __FILE__, __LINE__ + 1 )
19
+ def #{ attr }( value = nil )
20
+ value and self.#{ attr } = value
21
+ @#{ attr } ||= default_#{ attr }
22
+ block_given? ? yield( @#{ attr } ) : @#{ attr }
23
+ end
24
+
25
+ def #{ attr }= value
26
+ @#{ attr } = Rectangle( value )
27
+ end
28
+ END
29
+ end
30
+
31
+ def alignment( value = nil )
32
+ value and self.alignment = value
33
+ @alignment or @owner ? @owner.alignment : :left
34
+ end
35
+
36
+ def alignment= value
37
+ ALIGNMENTS.member?( value = value.to_sym ) or
38
+ raise( ArgumentError, "unkown alignment: %p" % value )
39
+ @alignment = value
40
+ end
41
+
42
+ def style( value = nil )
43
+ value and self.style = value
44
+ @style or @owner ? @owner.style : Graphics::NAMED_STYLES[ 'ascii' ]
45
+ end
46
+
47
+ def style= value
48
+ @style = Style( value )
49
+ end
50
+
51
+ def render( output = @output )
52
+ if output
53
+ render_content( output )
54
+ return output
55
+ else
56
+ OutputDevice.buffer do | out |
57
+ render_content( out )
58
+ end
59
+ end
60
+ end
61
+
62
+ def to_s
63
+ OutputDevice.buffer do | out |
64
+ render_content( out )
65
+ end
66
+ end
67
+
68
+ def height
69
+ @height or calculate_height
70
+ end
71
+
72
+ def width
73
+ @width or calculate_width
74
+ end
75
+
76
+ attr_writer :max_width
77
+
78
+ def max_width
79
+ @max_width or @owner && @owner.max_width or output.width
80
+ end
81
+
82
+ def output
83
+ @output ||= ( @owner and @owner.output or OutputDevice.stdout )
84
+ end
85
+
86
+ def output=( io )
87
+ @output = io.nil? ? io : Output( io )
88
+ end
89
+
90
+ private
91
+
92
+ def initialize_view( options = nil, owner = nil )
93
+ @max_width = @width = @height = nil
94
+ @margin = @padding = @alignment = @style = nil
95
+ @output = @foreground = @background = nil
96
+
97
+ if options
98
+ val = options[ :width ] and self.width = val
99
+ val = options[ :align ] and self.alignment = val
100
+ val = options[ :style ] and self.style = val
101
+ val = options[ :padding ] and self.padding = val
102
+ val = options[ :margin ] and self.margin = val
103
+ val = options[ :output ] and self.output = val
104
+ end
105
+
106
+ @owner = owner
107
+ end
108
+
109
+ def default_margin
110
+ Rectangle.new( 0, 0, 0, 0 )
111
+ end
112
+
113
+ def default_padding
114
+ Rectangle.new( 0, 0, 0, 0 )
115
+ end
116
+
117
+ def default_border
118
+ Rectangle.new( false, false, false, false )
119
+ end
120
+
121
+ end
122
+
123
+ module Presentation::ClassMethods
124
+ def default( property, value = nil, &dynamic )
125
+ if dynamic
126
+ define_method( :"default_#{property}", &dynamic )
127
+ else
128
+ define_method( :"default_#{property}" ) { value }
129
+ end
130
+ end
131
+ end
132
+ end