monocle-print 1.0.0

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,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