ohboyohboyohboy-monocle-print 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +3 -0
- data/History.txt +4 -0
- data/Manifest.txt +21 -0
- data/README.txt +40 -0
- data/Rakefile +21 -0
- data/lib/monocle-print.rb +87 -0
- data/lib/monocle-print/atomic.rb +461 -0
- data/lib/monocle-print/geometry.rb +66 -0
- data/lib/monocle-print/graphics.rb +86 -0
- data/lib/monocle-print/graphics/registry.rb +63 -0
- data/lib/monocle-print/layout.rb +58 -0
- data/lib/monocle-print/list.rb +138 -0
- data/lib/monocle-print/output-device.rb +529 -0
- data/lib/monocle-print/presentation.rb +132 -0
- data/lib/monocle-print/progress.rb +192 -0
- data/lib/monocle-print/table.rb +229 -0
- data/lib/monocle-print/table/column.rb +98 -0
- data/lib/monocle-print/table/members.rb +252 -0
- data/lib/monocle-print/table/segments.rb +62 -0
- data/lib/monocle-print/terminal-escapes.rb +205 -0
- data/lib/monocle-print/utils.rb +23 -0
- metadata +98 -0
@@ -0,0 +1,66 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# encoding: utf-8
|
3
|
+
|
4
|
+
module MonoclePrint
|
5
|
+
Pair = Struct.new( :x, :y )
|
6
|
+
class Pair
|
7
|
+
alternate_names =
|
8
|
+
{
|
9
|
+
:x => %w( width column left ),
|
10
|
+
:y => %w( height line right )
|
11
|
+
}
|
12
|
+
|
13
|
+
alternate_names.each do | field, name_list |
|
14
|
+
name_list.each do | name |
|
15
|
+
alias_method( name, field )
|
16
|
+
alias_method( "#{ name }=", "#{ field }=" )
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize( x = 0, y = 0 )
|
21
|
+
super
|
22
|
+
end
|
23
|
+
|
24
|
+
def +@
|
25
|
+
self.column = 0
|
26
|
+
self.line += 1
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
def ~
|
31
|
+
self.column = 0
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def -@
|
36
|
+
self.column = 0
|
37
|
+
self.line -= 1
|
38
|
+
self
|
39
|
+
end
|
40
|
+
|
41
|
+
def +( n )
|
42
|
+
self.column += n
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
def -( n )
|
47
|
+
self.column -= n
|
48
|
+
self
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
Rectangle = Struct.new( :left, :top, :right, :bottom )
|
53
|
+
class Rectangle
|
54
|
+
def self.create( params )
|
55
|
+
left = params.fetch( :left, 0 )
|
56
|
+
right = params.fetch( :right, left )
|
57
|
+
top = params.fetch( :top, left )
|
58
|
+
bottom = params.fetch( :bottom, top )
|
59
|
+
new( left, top, right, bottom )
|
60
|
+
end
|
61
|
+
|
62
|
+
def initialize( left = 0, top = left, right = left, bottom = top )
|
63
|
+
super( left, top, right, bottom )
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# encoding: utf-8
|
3
|
+
|
4
|
+
require 'monocle-print/graphics/registry'
|
5
|
+
|
6
|
+
module MonoclePrint
|
7
|
+
Graphics =
|
8
|
+
Struct.new(
|
9
|
+
:ew, # west east - horizontal line
|
10
|
+
:ns, # north south - vertical line
|
11
|
+
:es, # north west - top right corner
|
12
|
+
:sw, # north east
|
13
|
+
:nw, # south east
|
14
|
+
:en, # south west
|
15
|
+
:ens, # north south east
|
16
|
+
:nsw, # north south west
|
17
|
+
:enw, # east north west
|
18
|
+
:esw, # east south west
|
19
|
+
:ensw, # east north south west - intersection of horiz & vertical lines
|
20
|
+
:tree_fork,
|
21
|
+
:tree_tail,
|
22
|
+
:blank
|
23
|
+
)
|
24
|
+
|
25
|
+
class Graphics
|
26
|
+
include MonoclePrint
|
27
|
+
extend GraphicsRegistry
|
28
|
+
|
29
|
+
define :blank, '', '', '', '', '', '', '', '', '', '', '', '', ''
|
30
|
+
define :ascii, '-', '|', '+', '+', '+', '+', '+', '+', '+', '+', '+', '|', '`'
|
31
|
+
define :single_line, '─', '│', '┌', '┐', '┘', '└', '├', '┤', '┴', '┬', '┼', '├', '└'
|
32
|
+
define :double_line, '═', '║', '╔', '╗', '╝', '╚', '╠', '╣', '╩', '╦', '╬', '╠', '╚'
|
33
|
+
|
34
|
+
def format( description )
|
35
|
+
out = Line( description )
|
36
|
+
out.gsub!( /<([nsewlrtbudhv]+)(?::(\d+))?>/i ) do
|
37
|
+
box_bit = resolve_name( $1 )
|
38
|
+
$2 ? box_bit.tile( $2.to_i ) : box_bit
|
39
|
+
end
|
40
|
+
return( out )
|
41
|
+
end
|
42
|
+
|
43
|
+
def horizontal_line( width )
|
44
|
+
ew.tile( width )
|
45
|
+
end
|
46
|
+
|
47
|
+
def box_top( width )
|
48
|
+
format( "<se><ew:#{ width }><sw>" )
|
49
|
+
end
|
50
|
+
|
51
|
+
def box_bottom( width )
|
52
|
+
format( "<ne><ew:#{ width }><nw>" )
|
53
|
+
end
|
54
|
+
|
55
|
+
def table_top( *column_widths )
|
56
|
+
nw + line_with_joints( esw, column_widths ) + ne
|
57
|
+
end
|
58
|
+
|
59
|
+
def table_divide( *column_widths )
|
60
|
+
ens + line_with_joints( ensw, column_widths ) + nsw
|
61
|
+
end
|
62
|
+
|
63
|
+
def table_bottom( *column_widths )
|
64
|
+
sw + line_with_joints( enw, column_widths ) + se
|
65
|
+
end
|
66
|
+
|
67
|
+
def line_with_joints( joint, *widths )
|
68
|
+
widths.map { | w | horizontal_line( w ) }.join( joint )
|
69
|
+
end
|
70
|
+
|
71
|
+
alias h ew
|
72
|
+
alias v ns
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def resolve_name( name )
|
77
|
+
name.downcase!
|
78
|
+
name.tr!( 'lrtbud', 'wensns' )
|
79
|
+
name.gsub!( 'h', 'ew' )
|
80
|
+
name.gsub!( 'v', 'ns' )
|
81
|
+
chars = name.chars.to_a.sort!
|
82
|
+
chars.uniq!
|
83
|
+
self[ chars.join('') ]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# encoding: utf-8
|
3
|
+
#
|
4
|
+
# author: Kyle Yetter
|
5
|
+
#
|
6
|
+
|
7
|
+
module MonoclePrint
|
8
|
+
module GraphicsRegistry
|
9
|
+
ENV_KEY = "MONOCLE_PRINT_STYLE"
|
10
|
+
FALLBACK_STYLE = "single_line"
|
11
|
+
|
12
|
+
attr_accessor :default_style
|
13
|
+
|
14
|
+
def named_styles
|
15
|
+
@named_styles ||= Hash.new { |h, k| h[ default_style ].dup }
|
16
|
+
end
|
17
|
+
|
18
|
+
def style?( name )
|
19
|
+
named_styles.key?( name.to_s )
|
20
|
+
end
|
21
|
+
|
22
|
+
def style( name )
|
23
|
+
named_styles[ name.to_s ]
|
24
|
+
end
|
25
|
+
|
26
|
+
def styles
|
27
|
+
named_styles.keys
|
28
|
+
end
|
29
|
+
|
30
|
+
def default_style
|
31
|
+
@default_style ||= detect_style_from_env
|
32
|
+
end
|
33
|
+
|
34
|
+
def default
|
35
|
+
style( default_style )
|
36
|
+
end
|
37
|
+
|
38
|
+
def define( name, *parts )
|
39
|
+
parts.map! { | p | Line( p ).freeze }
|
40
|
+
name = name.to_s
|
41
|
+
definition = new( *parts ).freeze
|
42
|
+
named_styles.store( name, definition )
|
43
|
+
define_singleton_method( name ) { style( name ) }
|
44
|
+
definition
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def detect_style_from_env
|
50
|
+
default_style = ENV.fetch( ENV_KEY, FALLBACK_STYLE )
|
51
|
+
unless style?( default_style )
|
52
|
+
message = <<-END.gsub!( /^\s*\| ?/, '' ).strip!.gsub!( /\s+/, ' ' )
|
53
|
+
| cannot set MonoclePrint's default graphics style
|
54
|
+
| from the MONOCLE_PRINT_STYLE environment variable as `%s'
|
55
|
+
| is not a known style; defaulting to `%s'
|
56
|
+
END
|
57
|
+
warn( message % [ default_style, FALLBACK_STYLE ] )
|
58
|
+
default_style = FALLBACK_STYLE
|
59
|
+
end
|
60
|
+
default_style
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# encoding: utf-8
|
3
|
+
|
4
|
+
module MonoclePrint
|
5
|
+
class Layout < Array
|
6
|
+
include Presentation
|
7
|
+
|
8
|
+
def initialize( options = nil )
|
9
|
+
initialize_view( options )
|
10
|
+
super( 0 )
|
11
|
+
block_given? and yield( self )
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class Horizontal < Layout
|
16
|
+
|
17
|
+
def width
|
18
|
+
inject( 0 ) do | width, view |
|
19
|
+
width + view.width
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def height
|
24
|
+
empty? ? 0 : collect { | view | view.height }.max
|
25
|
+
end
|
26
|
+
|
27
|
+
def render
|
28
|
+
map { | i | i.render }.inject do | out, text |
|
29
|
+
out.juxtapose!( text )
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
class Vertical < Layout
|
36
|
+
|
37
|
+
def width
|
38
|
+
empty? ? 0 : collect { | view | view.width }.max
|
39
|
+
end
|
40
|
+
|
41
|
+
def height
|
42
|
+
inject( 0 ) do | height, view |
|
43
|
+
height + view.height
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def render
|
48
|
+
map { | i | i.render }.inject do | out, text |
|
49
|
+
out.concat( text )
|
50
|
+
end.fix!
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
class Flow < Layout
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# encoding: utf-8
|
3
|
+
|
4
|
+
module MonoclePrint
|
5
|
+
class List
|
6
|
+
include Presentation
|
7
|
+
|
8
|
+
def initialize( *items )
|
9
|
+
@spacer = nil
|
10
|
+
@columns = nil
|
11
|
+
|
12
|
+
items = [ items ].flatten!
|
13
|
+
options = items.last.is_a?( Hash ) ? items.pop : nil
|
14
|
+
initialize_view( options )
|
15
|
+
|
16
|
+
@items = items.map! { | item | SingleLine.new( item ) }
|
17
|
+
@sorted = nil
|
18
|
+
|
19
|
+
if options
|
20
|
+
value = options[ :column_spacer ] and self.column_spacing = value
|
21
|
+
value = options[ :columns ] and self.columns = value
|
22
|
+
end
|
23
|
+
block_given? and yield( self )
|
24
|
+
end
|
25
|
+
|
26
|
+
def spacer( value = nil )
|
27
|
+
value and self.column_spacer = value
|
28
|
+
@spacer or SingleLine.new( ' ' )
|
29
|
+
end
|
30
|
+
|
31
|
+
def spacer= value
|
32
|
+
@spacer =
|
33
|
+
case value
|
34
|
+
when nil, SingleLine then value
|
35
|
+
else SingleLine.new( value )
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def spacing( n )
|
40
|
+
self.spacing = n
|
41
|
+
spacer.width
|
42
|
+
end
|
43
|
+
|
44
|
+
def spacing= width
|
45
|
+
self.spacer = ' ' * Utils.at_least( width.to_i, 0 )
|
46
|
+
end
|
47
|
+
|
48
|
+
def columns( n = nil )
|
49
|
+
n and self.columns = n
|
50
|
+
@columns or optimize_columns
|
51
|
+
end
|
52
|
+
|
53
|
+
def columns= n
|
54
|
+
@columns =
|
55
|
+
case n
|
56
|
+
when nil then n
|
57
|
+
else Utils.at_least( n.to_i, 1 )
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def render_content( output )
|
64
|
+
unless columns = @columns
|
65
|
+
@sorted = sorted
|
66
|
+
columns = optimize_columns
|
67
|
+
@sorted = nil
|
68
|
+
end
|
69
|
+
|
70
|
+
widths = calculate_columns( columns )
|
71
|
+
number_of_rows = ( @items.length.to_f / columns ).ceil
|
72
|
+
|
73
|
+
each_column = @items.each_slice( number_of_rows )
|
74
|
+
columns = each_column.zip( widths ).map do | cells, width |
|
75
|
+
if cells.length < number_of_rows
|
76
|
+
cells.fill( SingleLine.new( '' ), cells.length,
|
77
|
+
number_of_rows - cells.length )
|
78
|
+
end
|
79
|
+
cells.map! { | cell | cell.align( alignment, width ) }
|
80
|
+
end
|
81
|
+
|
82
|
+
for row in columns.transpose
|
83
|
+
output.puts( row.join( spacer ) )
|
84
|
+
end
|
85
|
+
return( output )
|
86
|
+
end
|
87
|
+
|
88
|
+
def sorted
|
89
|
+
@sorted or @items.each_with_index.sort_by { | item, index | -item.width }
|
90
|
+
end
|
91
|
+
|
92
|
+
def optimize_columns
|
93
|
+
limit = max_width
|
94
|
+
@items.empty? and return( nil )
|
95
|
+
c, rem = @items.length.divmod( 2 )
|
96
|
+
rem.zero? or c += 1
|
97
|
+
column_spacing = spacer.width
|
98
|
+
|
99
|
+
best_columns = 1
|
100
|
+
for number_of_rows in ( 1 .. c )
|
101
|
+
number_of_columns = ( @items.length.to_f / number_of_rows ).ceil
|
102
|
+
columns = calculate_columns( number_of_columns )
|
103
|
+
total_width = columns.inject( ( number_of_columns - 1 ) * column_spacing, :+ )
|
104
|
+
if total_width <= limit
|
105
|
+
best_columns = number_of_columns
|
106
|
+
break
|
107
|
+
end
|
108
|
+
end
|
109
|
+
return( best_columns )
|
110
|
+
end
|
111
|
+
|
112
|
+
#
|
113
|
+
# create a fixed number of columns, calculating respsective
|
114
|
+
# widths and indices within the list
|
115
|
+
#
|
116
|
+
def calculate_columns( n )
|
117
|
+
rows, rem = @items.length.divmod( n )
|
118
|
+
rem.zero? or rows += 1
|
119
|
+
columns = Array.new( n )
|
120
|
+
items = sorted.dup
|
121
|
+
# items is now ordered from widest to thinnest, along with respective indices
|
122
|
+
|
123
|
+
# collect n columns and their respective width metrics,
|
124
|
+
# working through items from widest down
|
125
|
+
n.times do
|
126
|
+
# if a column has already been marked with a width,
|
127
|
+
# repeat this block
|
128
|
+
begin
|
129
|
+
item, index = items.shift
|
130
|
+
column_number = index / rows
|
131
|
+
end while columns[ column_number ]
|
132
|
+
columns[ column_number ] = item.width
|
133
|
+
end
|
134
|
+
return( columns )
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,529 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# encoding: utf-8
|
3
|
+
|
4
|
+
|
5
|
+
module MonoclePrint
|
6
|
+
class OutputDevice < DelegateClass( IO )
|
7
|
+
include MonoclePrint
|
8
|
+
include TerminalEscapes
|
9
|
+
|
10
|
+
def self.open( path, options = {} )
|
11
|
+
File.open( path, options.fetch( :mode, 'w' ) ) do | file |
|
12
|
+
return( yield( new( file, options ) ) )
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.buffer( options = {} )
|
17
|
+
case options
|
18
|
+
when Numeric then options = { :width => options }
|
19
|
+
when nil then options = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
buffer = StringIO.new( '', options.fetch( :mode, 'w' ) )
|
23
|
+
out = new( buffer, options )
|
24
|
+
if block_given?
|
25
|
+
begin
|
26
|
+
yield( out )
|
27
|
+
return( out.string )
|
28
|
+
ensure
|
29
|
+
out.close
|
30
|
+
end
|
31
|
+
else
|
32
|
+
return out
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.stdout( options = {} )
|
37
|
+
device = new( $stdout, options )
|
38
|
+
block_given? ? yield( device ) : device
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.stderr( options = {} )
|
42
|
+
device = new( $stderr, options )
|
43
|
+
block_given? ? yield( device ) : device
|
44
|
+
end
|
45
|
+
|
46
|
+
DEFAULT_SIZE = Pair.new( 80, 22 ).freeze
|
47
|
+
IO_PRINT_METHODS = %w( puts print printf putc )
|
48
|
+
SIZE_IOCTL = 0x5413
|
49
|
+
SIZE_STRUCT = [ 0, 0, 0, 0 ].pack( "SSSS" ).freeze
|
50
|
+
|
51
|
+
private :reopen
|
52
|
+
attr_reader :device
|
53
|
+
attr_accessor :style
|
54
|
+
|
55
|
+
def initialize( output, options = {} )
|
56
|
+
@device =
|
57
|
+
case output
|
58
|
+
when String then File.open( output, options.fetch( :mode, 'w' ) )
|
59
|
+
when IO then output
|
60
|
+
else
|
61
|
+
if IO_PRINT_METHODS.all? { | m | output.respond_to?( m ) }
|
62
|
+
output
|
63
|
+
else
|
64
|
+
msg = "%s requires an IO-like object, but was given: %p" % [ self.class, output ]
|
65
|
+
raise( ArgumentError, msg )
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
super( @device )
|
70
|
+
|
71
|
+
@background_stack = []
|
72
|
+
@foreground_stack = []
|
73
|
+
@modifier_stack = []
|
74
|
+
|
75
|
+
@background = @foreground = @modifier = nil
|
76
|
+
@use_color = options.fetch( :use_color, tty? )
|
77
|
+
|
78
|
+
@cursor = Pair.new( 0, 0 )
|
79
|
+
@tab_width = options.fetch( :tab_width, 8 )
|
80
|
+
@margin = Pair.new( 0, 0 )
|
81
|
+
|
82
|
+
@newline = options.fetch( :newline, $/ )
|
83
|
+
|
84
|
+
case margin = options.fetch( :margin, 0 )
|
85
|
+
when Numeric then @margin.left = @margin.right = margin.to_i
|
86
|
+
else
|
87
|
+
@margin.left = margin[ :left ].to_i
|
88
|
+
@margin.right = margin[ :right ].to_i
|
89
|
+
end
|
90
|
+
|
91
|
+
@tabs = {}
|
92
|
+
@screen_size = nil
|
93
|
+
@forced_width = options[ :width ]
|
94
|
+
@forced_height = options[ :height ]
|
95
|
+
@alignment = options.fetch( :alignment, :left )
|
96
|
+
@style = Style( options[ :style ] )
|
97
|
+
end
|
98
|
+
|
99
|
+
def foreground( color = nil )
|
100
|
+
color or return( @foreground_stack.last )
|
101
|
+
begin
|
102
|
+
@foreground_stack.push( color )
|
103
|
+
yield
|
104
|
+
ensure
|
105
|
+
@foreground_stack.pop
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def colors( fg, bg )
|
110
|
+
foreground( fg ) { background( bg ) { yield } }
|
111
|
+
end
|
112
|
+
|
113
|
+
def background( color = nil )
|
114
|
+
color or return( @background_stack.last )
|
115
|
+
begin
|
116
|
+
@background_stack.push( color )
|
117
|
+
yield
|
118
|
+
ensure
|
119
|
+
@background_stack.pop
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
alias on background
|
124
|
+
|
125
|
+
def modifier( name = nil )
|
126
|
+
name or return( @modifier_stack.last )
|
127
|
+
begin
|
128
|
+
@modifier_stack.push( name )
|
129
|
+
yield
|
130
|
+
ensure
|
131
|
+
@modifier_stack.pop
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
for color in ANSI_COLOR_NAMES
|
136
|
+
class_eval( <<-END, __FILE__, __LINE__ )
|
137
|
+
def #{ color }
|
138
|
+
foreground( :#{ color } ) { yield }
|
139
|
+
end
|
140
|
+
|
141
|
+
def on_#{ color }
|
142
|
+
background( :#{ color } ) { yield }
|
143
|
+
end
|
144
|
+
END
|
145
|
+
end
|
146
|
+
|
147
|
+
for modifier in ANSI_MODIFIER_NAMES
|
148
|
+
class_eval( <<-END, __FILE__, __LINE__ )
|
149
|
+
def #{ modifier }
|
150
|
+
modifier( :#{ modifier } ) { yield }
|
151
|
+
end
|
152
|
+
END
|
153
|
+
end
|
154
|
+
|
155
|
+
def use_color?
|
156
|
+
@use_color
|
157
|
+
end
|
158
|
+
|
159
|
+
def use_color( v = nil )
|
160
|
+
v.nil? or self.use_color = v
|
161
|
+
return( @use_color )
|
162
|
+
end
|
163
|
+
|
164
|
+
def use_color= v
|
165
|
+
@use_color = !!v
|
166
|
+
end
|
167
|
+
|
168
|
+
def margin= n
|
169
|
+
self.left_margin = self.right_margin = Utils.at_least( n.to_i, 0 )
|
170
|
+
end
|
171
|
+
|
172
|
+
def indent( n = 0, m = 0 )
|
173
|
+
n, m = n.to_i, m.to_i
|
174
|
+
self.left_margin += n
|
175
|
+
self.right_margin += m
|
176
|
+
if block_given?
|
177
|
+
begin
|
178
|
+
yield
|
179
|
+
ensure
|
180
|
+
self.left_margin -= n
|
181
|
+
self.right_margin -= m
|
182
|
+
end
|
183
|
+
else
|
184
|
+
self
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def outdent( n )
|
189
|
+
self.left_margin -= n.to_i
|
190
|
+
block_given? and
|
191
|
+
begin yield
|
192
|
+
ensure self.left_margin += n.to_i
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def left_margin
|
197
|
+
@margin.left
|
198
|
+
end
|
199
|
+
|
200
|
+
def right_margin
|
201
|
+
@margin.right
|
202
|
+
end
|
203
|
+
|
204
|
+
def left_margin= n
|
205
|
+
@margin.left = n.to_i
|
206
|
+
end
|
207
|
+
|
208
|
+
def right_margin= n
|
209
|
+
@margin.right = n.to_i
|
210
|
+
end
|
211
|
+
|
212
|
+
def reset!
|
213
|
+
@fg_stack.clear
|
214
|
+
@bg_stack.clear
|
215
|
+
@margin = Pair.new( 0, 0 )
|
216
|
+
@cursor = Pair.new( 0, 0 )
|
217
|
+
@screen_size = nil
|
218
|
+
end
|
219
|
+
|
220
|
+
alias use_color? use_color
|
221
|
+
|
222
|
+
def list( *args )
|
223
|
+
List.new( *args ) do | list |
|
224
|
+
list.output = self
|
225
|
+
block_given? and yield( list )
|
226
|
+
list.render
|
227
|
+
end
|
228
|
+
return( self )
|
229
|
+
end
|
230
|
+
|
231
|
+
def table( *args )
|
232
|
+
Table.new( *args ) do | t |
|
233
|
+
block_given? and yield( t )
|
234
|
+
t.render( self )
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
def column_layout( *args )
|
239
|
+
ColumnLayout.new( *args ) do | t |
|
240
|
+
block_given? and yield( t )
|
241
|
+
t.render( self )
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
def leger( char = '<h>', w = width )
|
246
|
+
puts( @style.format( char ).tile( w ) )
|
247
|
+
end
|
248
|
+
|
249
|
+
def print( *objs )
|
250
|
+
text = Text( [ objs ].flatten!.join )
|
251
|
+
@use_color or text.bleach!
|
252
|
+
last_line = text.pop
|
253
|
+
for line in text
|
254
|
+
put!( line )
|
255
|
+
end
|
256
|
+
fill( @margin.left )
|
257
|
+
|
258
|
+
@device.print( color_code )
|
259
|
+
@cursor + last_line.width
|
260
|
+
@device.print( last_line )
|
261
|
+
@device.print( clear_attr )
|
262
|
+
|
263
|
+
self
|
264
|
+
end
|
265
|
+
|
266
|
+
def puts( *objs )
|
267
|
+
text = Text( [ objs ].flatten!.join( @newline ) )
|
268
|
+
( text.empty? or text.last.empty? ) and text << Line( '' )
|
269
|
+
for line in text
|
270
|
+
put( line )
|
271
|
+
newline!
|
272
|
+
end
|
273
|
+
self
|
274
|
+
end
|
275
|
+
|
276
|
+
def printf( fmt, *args )
|
277
|
+
print( sprintf( fmt, *args ) )
|
278
|
+
end
|
279
|
+
|
280
|
+
def putsf( fmt, *args )
|
281
|
+
puts( sprintf( fmt, *args ) )
|
282
|
+
end
|
283
|
+
|
284
|
+
for m in %w( left right center )
|
285
|
+
class_eval( <<-END, __FILE__, __LINE__ + 1 )
|
286
|
+
def #{ m }
|
287
|
+
prior, @alignment = @alignment, :#{ m }
|
288
|
+
yield
|
289
|
+
ensure
|
290
|
+
@alignment = prior
|
291
|
+
end
|
292
|
+
END
|
293
|
+
end
|
294
|
+
|
295
|
+
def close
|
296
|
+
@device.close rescue nil
|
297
|
+
end
|
298
|
+
|
299
|
+
def height
|
300
|
+
screen_size.height
|
301
|
+
end
|
302
|
+
|
303
|
+
def full_width
|
304
|
+
screen_size.width
|
305
|
+
end
|
306
|
+
|
307
|
+
def width
|
308
|
+
screen_size.width - @margin.left - @margin.right
|
309
|
+
end
|
310
|
+
|
311
|
+
def put( str, options = nil )
|
312
|
+
if options
|
313
|
+
fill = @style.format( options.fetch( :fill, ' ' ) )
|
314
|
+
align = options.fetch( :align, @alignment )
|
315
|
+
else
|
316
|
+
fill, align = ' ', @alignment
|
317
|
+
end
|
318
|
+
|
319
|
+
str = SingleLine.new( str ).align!( align, width, fill )
|
320
|
+
code = color_code
|
321
|
+
str.gsub!( /\e\[0m/ ) { "\e[0m" << code }
|
322
|
+
|
323
|
+
fill( @margin.left )
|
324
|
+
@device.print( code )
|
325
|
+
@device.print( str )
|
326
|
+
@cursor + str.width
|
327
|
+
@device.print( clear_attr )
|
328
|
+
fill( screen_size.width - @cursor.column )
|
329
|
+
self
|
330
|
+
end
|
331
|
+
|
332
|
+
def put!( str, options = nil )
|
333
|
+
put( str, options )
|
334
|
+
newline!
|
335
|
+
end
|
336
|
+
|
337
|
+
def return!
|
338
|
+
~@cursor
|
339
|
+
@device.print("\r")
|
340
|
+
@device.flush
|
341
|
+
end
|
342
|
+
|
343
|
+
def fill( width, char = ' ' )
|
344
|
+
width =
|
345
|
+
case width
|
346
|
+
when Symbol, String
|
347
|
+
distance_to( width )
|
348
|
+
when Fixnum
|
349
|
+
Utils.at_least( width, 0 )
|
350
|
+
end
|
351
|
+
if width > 0
|
352
|
+
fill_str =
|
353
|
+
if char.length > 1 and ( char = SingleLine( char ) ).width > 1
|
354
|
+
char.tile( width )
|
355
|
+
else
|
356
|
+
char * width
|
357
|
+
end
|
358
|
+
@cursor.column += width
|
359
|
+
@device.print( fill_str )
|
360
|
+
end
|
361
|
+
self
|
362
|
+
end
|
363
|
+
|
364
|
+
def newline!
|
365
|
+
+@cursor
|
366
|
+
@device.print( @newline )
|
367
|
+
@device.flush
|
368
|
+
self
|
369
|
+
end
|
370
|
+
|
371
|
+
def space( n_lines = 1 )
|
372
|
+
n_lines.times do
|
373
|
+
put( '' )
|
374
|
+
newline!
|
375
|
+
end
|
376
|
+
self
|
377
|
+
end
|
378
|
+
|
379
|
+
def column
|
380
|
+
@cursor.column
|
381
|
+
end
|
382
|
+
|
383
|
+
def line
|
384
|
+
@cursor.line
|
385
|
+
end
|
386
|
+
|
387
|
+
def clear
|
388
|
+
@cursor.line = @cursor.column = 0
|
389
|
+
@device.print( set_cursor( 0, 0 ), clear_screen )
|
390
|
+
@device.flush
|
391
|
+
return( self )
|
392
|
+
end
|
393
|
+
|
394
|
+
def clear_line
|
395
|
+
@device.print( super )
|
396
|
+
@device.flush
|
397
|
+
return!
|
398
|
+
end
|
399
|
+
|
400
|
+
for m in %w( horizontal_line box_top box_bottom )
|
401
|
+
class_eval( <<-END, __FILE__, __LINE__ + 1 )
|
402
|
+
def #{ m }
|
403
|
+
put( @style.#{ m }( width ) )
|
404
|
+
newline!
|
405
|
+
end
|
406
|
+
END
|
407
|
+
end
|
408
|
+
|
409
|
+
def color_code
|
410
|
+
@use_color or return ''
|
411
|
+
code = ''
|
412
|
+
|
413
|
+
case fg = @foreground_stack.last
|
414
|
+
when Fixnum then code << xterm_color( ?f, fg )
|
415
|
+
when String, Symbol then code << ansi_color( ?f, fg )
|
416
|
+
end
|
417
|
+
|
418
|
+
case bg = @background_stack.last
|
419
|
+
when Fixnum then code << xterm_color( ?b, bg )
|
420
|
+
when String, Symbol then code << ansi_color( ?b, bg )
|
421
|
+
end
|
422
|
+
|
423
|
+
case mod = @modifier_stack.last
|
424
|
+
when String, Symbol then code << ansi_modifier( mod )
|
425
|
+
end
|
426
|
+
|
427
|
+
code
|
428
|
+
end
|
429
|
+
|
430
|
+
private
|
431
|
+
|
432
|
+
def abs( col )
|
433
|
+
col < 0 and col += width
|
434
|
+
Utils.bound( col, 0, width - 1 )
|
435
|
+
end
|
436
|
+
|
437
|
+
|
438
|
+
def distance_to( tab )
|
439
|
+
Utils.at_least( abs( @tabs[ tab ] ) - @cursor.columm, 0 )
|
440
|
+
end
|
441
|
+
|
442
|
+
def screen_size
|
443
|
+
@screen_size ||=
|
444
|
+
begin
|
445
|
+
if device.respond_to?( :winsize )
|
446
|
+
detected_height, detected_width = device.winsize
|
447
|
+
elsif data = SIZE_STRUCT.dup and device.ioctl( SIZE_IOCTL, data ) >= 0
|
448
|
+
detected_height, detected_width = data.unpack( "SS" )
|
449
|
+
else
|
450
|
+
size = default_size
|
451
|
+
detected_height, detected_width = size.height, size.width
|
452
|
+
end
|
453
|
+
Pair.new(
|
454
|
+
@forced_width || detected_width,
|
455
|
+
@forced_height || detected_height
|
456
|
+
)
|
457
|
+
rescue Exception
|
458
|
+
default_size
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
def default_size
|
463
|
+
Pair.new( default_width, default_height )
|
464
|
+
end
|
465
|
+
|
466
|
+
def default_height
|
467
|
+
( @forced_height || ENV[ 'LINES' ] || DEFAULT_SIZE.height ).to_i
|
468
|
+
end
|
469
|
+
|
470
|
+
def default_width
|
471
|
+
( @forced_width || ENV[ 'COLUMNS' ] || DEFAULT_SIZE.width ).to_i
|
472
|
+
end
|
473
|
+
end
|
474
|
+
|
475
|
+
class Pager < OutputDevice
|
476
|
+
unless pager_command = ENV['PAGER']
|
477
|
+
pagers = %w( most less more )
|
478
|
+
system_path = ENV[ "PATH" ].split( File::PATH_SEPARATOR )
|
479
|
+
pager_command = pagers.find do | cmd |
|
480
|
+
system_path.find { | dir | test( ?x, File.join( dir, cmd ) ) }
|
481
|
+
end
|
482
|
+
end
|
483
|
+
PAGER_COMMAND = pager_command
|
484
|
+
|
485
|
+
def self.open( options = {} )
|
486
|
+
unless PAGER_COMMAND
|
487
|
+
message = <<-END.gsub!( /\s+/, ' ' ).strip!
|
488
|
+
unable to locate a pager program on the system's PATH or from
|
489
|
+
the environmental variable, PAGER
|
490
|
+
END
|
491
|
+
raise( IOError, message )
|
492
|
+
end
|
493
|
+
|
494
|
+
options.fetch( :use_color ) { options[ :use_color ] = true }
|
495
|
+
|
496
|
+
if block_given?
|
497
|
+
IO.popen( PAGER_COMMAND, 'w' ) do | pager |
|
498
|
+
pager = new( pager, options )
|
499
|
+
return yield( pager )
|
500
|
+
end
|
501
|
+
else
|
502
|
+
return new( IO.popen( PAGER_COMMAND, 'w' ), options )
|
503
|
+
end
|
504
|
+
end
|
505
|
+
|
506
|
+
private
|
507
|
+
|
508
|
+
def screen_size
|
509
|
+
@screen_size ||=
|
510
|
+
begin
|
511
|
+
if STDOUT.respond_to?( :winsize )
|
512
|
+
detected_height, detected_width = STDOUT.winsize
|
513
|
+
elsif data = SIZE_STRUCT.dup and STDOUT.ioctl( SIZE_IOCTL, data ) >= 0
|
514
|
+
detected_height, detected_width = data.unpack( "SS" )
|
515
|
+
else
|
516
|
+
size = default_size
|
517
|
+
detected_height, detected_width = size.height, size.width
|
518
|
+
end
|
519
|
+
Pair.new(
|
520
|
+
@forced_height || detected_height,
|
521
|
+
@forced_width || detected_width
|
522
|
+
)
|
523
|
+
rescue Exception
|
524
|
+
default_size
|
525
|
+
end
|
526
|
+
end
|
527
|
+
end
|
528
|
+
|
529
|
+
end
|