ohboyohboyohboy-monocle-print 1.1.1
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.
- 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
|