ansi-sys-revived 0.8.4
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.
- data/lib/ansisys.rb +752 -0
- metadata +46 -0
data/lib/ansisys.rb
ADDED
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
#
|
|
2
|
+
# = ansisys.rb
|
|
3
|
+
# ANSI terminal emulator
|
|
4
|
+
# based on http://en.wikipedia.org/wiki/ANSI_escape_code
|
|
5
|
+
#
|
|
6
|
+
# Copyright:: Copyright (C) 2007 zunda <zunda at freeshell.org>
|
|
7
|
+
# License:: GPL - GPL3 or later
|
|
8
|
+
#
|
|
9
|
+
require 'webrick'
|
|
10
|
+
require 'nkf'
|
|
11
|
+
|
|
12
|
+
module AnsiSys
|
|
13
|
+
module VERSION #:nodoc:
|
|
14
|
+
MAJOR = 0
|
|
15
|
+
MINOR = 8
|
|
16
|
+
TINY = 4
|
|
17
|
+
|
|
18
|
+
STRING = [MAJOR, MINOR, TINY].join('.')
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
module CSSFormatter
|
|
22
|
+
# make a CSS style-let from a Hash of CSS settings
|
|
23
|
+
def hash_to_styles(hash, separator = '; ')
|
|
24
|
+
unless hash.empty?
|
|
25
|
+
return hash.map{|e| "#{e[0]}: #{e[1].join(' ')}"}.join(separator)
|
|
26
|
+
else
|
|
27
|
+
return nil
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
module_function :hash_to_styles
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
module Guess
|
|
34
|
+
# returns a $KCODE string according to guessed encoding
|
|
35
|
+
def self.kcode(data)
|
|
36
|
+
case NKF.guess(data)
|
|
37
|
+
when NKF::EUC
|
|
38
|
+
kcode = 'e'
|
|
39
|
+
when NKF::SJIS
|
|
40
|
+
kcode = 's'
|
|
41
|
+
when NKF::UTF8
|
|
42
|
+
kcode = 'u'
|
|
43
|
+
else
|
|
44
|
+
kcode = 'n'
|
|
45
|
+
end
|
|
46
|
+
return kcode
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
class AnsiSysError < StandardError; end
|
|
51
|
+
|
|
52
|
+
class Lexer
|
|
53
|
+
# Control Sequence Introducer and following code
|
|
54
|
+
PARAMETER_AND_LETTER = /\A([\d;]*)([[:alpha:]])/o
|
|
55
|
+
CODE_EQUIVALENT = {
|
|
56
|
+
"\r" => ['B'],
|
|
57
|
+
"\n" => ['E'],
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
attr_reader :buffer
|
|
61
|
+
|
|
62
|
+
# _csis_ is an Array of Code Sequence Introducers
|
|
63
|
+
# which can be \e[, \x9B, or both
|
|
64
|
+
def initialize(csis = ["\x1b["]) # CSI can also be "\x9B"
|
|
65
|
+
@code_start_re = Regexp.union(*(CODE_EQUIVALENT.keys + csis))
|
|
66
|
+
@buffer = ''
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# add the String (clear text with some or no escape sequences) to buffer
|
|
70
|
+
def push(string)
|
|
71
|
+
@buffer += string
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# returns array of tokens while deleting the tokenized part from buffer
|
|
75
|
+
def lex!
|
|
76
|
+
r = Array.new
|
|
77
|
+
@buffer.gsub!(/(?:\r\n|\n\r)/, "\n")
|
|
78
|
+
while @code_start_re =~ @buffer
|
|
79
|
+
r << [:string, $`] unless $`.empty?
|
|
80
|
+
if CODE_EQUIVALENT.has_key?($&)
|
|
81
|
+
CODE_EQUIVALENT[$&].each do |c|
|
|
82
|
+
r << [:code, c]
|
|
83
|
+
end
|
|
84
|
+
@buffer = $'
|
|
85
|
+
else
|
|
86
|
+
csi = $&
|
|
87
|
+
residual = $'
|
|
88
|
+
if PARAMETER_AND_LETTER =~ residual
|
|
89
|
+
r << [:code, $&]
|
|
90
|
+
@buffer = $'
|
|
91
|
+
else
|
|
92
|
+
@buffer = csi + residual
|
|
93
|
+
return r
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
r << [:string, @buffer] unless @buffer.empty?
|
|
98
|
+
@buffer = ''
|
|
99
|
+
return r
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
class Characters
|
|
104
|
+
# widths of characters
|
|
105
|
+
WIDTHS = {
|
|
106
|
+
"\t" => 8,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
attr_reader :string # clear text
|
|
110
|
+
attr_reader :sgr # Select Graphic Rendition associated with the text
|
|
111
|
+
|
|
112
|
+
def initialize(string, sgr)
|
|
113
|
+
@string = string
|
|
114
|
+
@sgr = sgr
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# echo the string onto the _screen_ with initial cursor as _cursor_
|
|
118
|
+
# _cursor_ position will be changed as the string is echoed
|
|
119
|
+
def echo_on(screen, cursor, kcode = nil)
|
|
120
|
+
each_char(kcode) do |c|
|
|
121
|
+
w = width(c)
|
|
122
|
+
cursor.fit!(w)
|
|
123
|
+
screen.write(c, w, cursor.cur_col, cursor.cur_row, @sgr.dup)
|
|
124
|
+
cursor.advance!(w)
|
|
125
|
+
end
|
|
126
|
+
return self
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
# iterator on each character
|
|
131
|
+
def each_char(kcode, &block)
|
|
132
|
+
@string.scan(Regexp.new('.', nil, kcode)).each do |c|
|
|
133
|
+
yield(c)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# width of a character
|
|
138
|
+
def width(char)
|
|
139
|
+
if WIDTHS.has_key?(char)
|
|
140
|
+
return WIDTHS[char]
|
|
141
|
+
end
|
|
142
|
+
case char.size # expecting number of bytes
|
|
143
|
+
when 1
|
|
144
|
+
return 1
|
|
145
|
+
else
|
|
146
|
+
return 2
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
class Cursor
|
|
152
|
+
# Escape sequence codes processed in this Class
|
|
153
|
+
CODE_LETTERS = %w(A B C D E F G H f)
|
|
154
|
+
|
|
155
|
+
attr_reader :cur_col # current column number (1-)
|
|
156
|
+
attr_reader :cur_row # current row number (1-)
|
|
157
|
+
attr_accessor :max_col # maximum column number
|
|
158
|
+
attr_accessor :max_row # maximum row number
|
|
159
|
+
|
|
160
|
+
def initialize(cur_col = 1, cur_row = 1, max_col = 80, max_row = 25)
|
|
161
|
+
@cur_col = cur_col
|
|
162
|
+
@cur_row = cur_row
|
|
163
|
+
@max_col = max_col
|
|
164
|
+
@max_row = max_row
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# applies self an escape sequence code that ends with _letter_ as String
|
|
168
|
+
# and with some _pars_ as Integers
|
|
169
|
+
def apply_code!(letter, *pars)
|
|
170
|
+
case letter
|
|
171
|
+
when 'A'
|
|
172
|
+
@cur_row -= pars[0] ? pars[0] : 1
|
|
173
|
+
@cur_row = @max_row if @max_row and @cur_row > @max_row
|
|
174
|
+
when 'B'
|
|
175
|
+
@cur_row += pars[0] ? pars[0] : 1
|
|
176
|
+
@cur_row = @max_row if @max_row and @cur_row > @max_row
|
|
177
|
+
when 'C'
|
|
178
|
+
@cur_col += pars[0] ? pars[0] : 1
|
|
179
|
+
when 'D'
|
|
180
|
+
@cur_col -= pars[0] ? pars[0] : 1
|
|
181
|
+
when 'E'
|
|
182
|
+
@cur_row += pars[0] ? pars[0] : 1
|
|
183
|
+
@cur_col = 1
|
|
184
|
+
@max_row = @cur_row if @max_row and @cur_row > @max_row
|
|
185
|
+
when 'F'
|
|
186
|
+
@cur_row -= pars[0] ? pars[0] : 1
|
|
187
|
+
@cur_col = 1
|
|
188
|
+
@max_row = @cur_row if @max_row and @cur_row > @max_row
|
|
189
|
+
when 'G'
|
|
190
|
+
@cur_col = pars[0] ? pars[0] : 1
|
|
191
|
+
when 'H'
|
|
192
|
+
@cur_row = pars[0] ? pars[0] : 1
|
|
193
|
+
@cur_col = pars[1] ? pars[1] : 1
|
|
194
|
+
@max_row = @cur_row if @max_row and @cur_row > @max_row
|
|
195
|
+
when 'f'
|
|
196
|
+
@cur_row = pars[0] ? pars[0] : 1
|
|
197
|
+
@cur_col = pars[1] ? pars[1] : 1
|
|
198
|
+
@max_row = @cur_row if @max_row and @cur_row > @max_row
|
|
199
|
+
end
|
|
200
|
+
if @cur_row < 1
|
|
201
|
+
@cur_row = 1
|
|
202
|
+
end
|
|
203
|
+
if @cur_col < 1
|
|
204
|
+
@cur_col = 1
|
|
205
|
+
elsif @cur_col > @max_col
|
|
206
|
+
@cur_col = @max_col
|
|
207
|
+
end
|
|
208
|
+
return self
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# changes current location for a character with _width_ to be echoed
|
|
212
|
+
def advance!(width = 1)
|
|
213
|
+
r = nil
|
|
214
|
+
@cur_col += width
|
|
215
|
+
if @cur_col > @max_col
|
|
216
|
+
line_feed!
|
|
217
|
+
r = "\n"
|
|
218
|
+
end
|
|
219
|
+
return r
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# check if a character with _width_ fits within the maximum columns,
|
|
223
|
+
# feed a line if not
|
|
224
|
+
def fit!(width = 1)
|
|
225
|
+
r = nil
|
|
226
|
+
if @cur_col + width > @max_col + 1
|
|
227
|
+
line_feed!
|
|
228
|
+
r = "\n"
|
|
229
|
+
end
|
|
230
|
+
return r
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# feed a line
|
|
234
|
+
def line_feed!
|
|
235
|
+
@cur_col = 1
|
|
236
|
+
@cur_row += 1
|
|
237
|
+
@max_row = @cur_row if @max_row and @cur_row > @max_row
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Select Graphic Rendition
|
|
242
|
+
class SGR
|
|
243
|
+
extend CSSFormatter
|
|
244
|
+
|
|
245
|
+
# Escape sequence codes processed in this Class
|
|
246
|
+
CODE_LETTERS = %w(m)
|
|
247
|
+
|
|
248
|
+
# :normal, :bold, or :faint
|
|
249
|
+
attr_reader :intensity
|
|
250
|
+
|
|
251
|
+
# :off or :on
|
|
252
|
+
attr_reader :italic
|
|
253
|
+
|
|
254
|
+
# :none, :single, or :double
|
|
255
|
+
attr_reader :underline
|
|
256
|
+
|
|
257
|
+
# :off, :slow, or :rapid
|
|
258
|
+
attr_reader :blink
|
|
259
|
+
|
|
260
|
+
# :positive or :negative
|
|
261
|
+
attr_reader :image
|
|
262
|
+
|
|
263
|
+
# :off or :on
|
|
264
|
+
attr_reader :conceal
|
|
265
|
+
|
|
266
|
+
# :black, :red, :green, :yellow, :blue, :magenta, :cyan, or :white
|
|
267
|
+
attr_reader :foreground
|
|
268
|
+
|
|
269
|
+
# :black, :red, :green, :yellow, :blue, :magenta, :cyan, or :white
|
|
270
|
+
attr_reader :background
|
|
271
|
+
|
|
272
|
+
def initialize
|
|
273
|
+
reset!
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# true if all the attributes are same
|
|
277
|
+
def ==(other)
|
|
278
|
+
instance_variables.each do |ivar|
|
|
279
|
+
return false unless instance_variable_get(ivar) == other.instance_variable_get(ivar)
|
|
280
|
+
end
|
|
281
|
+
return true
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# resets attributes
|
|
285
|
+
def reset!
|
|
286
|
+
apply_code!('m', 0)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# applies self an escape sequence code that ends with _letter_ as String
|
|
290
|
+
# and with some _pars_ as Integers
|
|
291
|
+
def apply_code!(letter = 'm', *pars)
|
|
292
|
+
raise AnsiSysError, "Invalid code for SGR: #{letter.inspect}" unless 'm' == letter
|
|
293
|
+
pars = [0] unless pars
|
|
294
|
+
pars.each do |code|
|
|
295
|
+
case code
|
|
296
|
+
when 0
|
|
297
|
+
@intensity = :normal
|
|
298
|
+
@italic = :off
|
|
299
|
+
@underline = :none
|
|
300
|
+
@blink = :off
|
|
301
|
+
@image = :positive
|
|
302
|
+
@conceal = :off
|
|
303
|
+
@foreground = :white
|
|
304
|
+
@background = :black
|
|
305
|
+
when 1..28
|
|
306
|
+
apply_code_table!(code)
|
|
307
|
+
when 30..37
|
|
308
|
+
@foreground = COLOR[code - 30]
|
|
309
|
+
@intensity = :normal
|
|
310
|
+
when 39
|
|
311
|
+
reset!
|
|
312
|
+
when 40..47
|
|
313
|
+
@background = COLOR[code - 40]
|
|
314
|
+
@intensity = :normal
|
|
315
|
+
when 49
|
|
316
|
+
reset!
|
|
317
|
+
when 90..97
|
|
318
|
+
@foreground = COLOR[code - 90]
|
|
319
|
+
@intensity = :bold
|
|
320
|
+
when 99
|
|
321
|
+
reset!
|
|
322
|
+
when 100..107
|
|
323
|
+
@background = COLOR[code - 100]
|
|
324
|
+
@intensity = :bold
|
|
325
|
+
when 109
|
|
326
|
+
reset!
|
|
327
|
+
else
|
|
328
|
+
raise AnsiSysError, "Invalid SGR code #{code.inspect}" unless CODE.has_key?(code)
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
return self
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# renders self as :html or :text _format_ - makes a <span> html scriptlet.
|
|
335
|
+
# _colors_ can be Screen.default_css_colors(_inverted_, _bright_).
|
|
336
|
+
def render(format = :html, position = :prefix, colors = Screen.default_css_colors)
|
|
337
|
+
case format
|
|
338
|
+
when :html
|
|
339
|
+
case position
|
|
340
|
+
when :prefix
|
|
341
|
+
style_code = css_style(colors)
|
|
342
|
+
if style_code
|
|
343
|
+
return %Q|<span style="#{style_code}">|
|
|
344
|
+
else
|
|
345
|
+
return ''
|
|
346
|
+
end
|
|
347
|
+
when :postfix
|
|
348
|
+
style_code = css_style(colors)
|
|
349
|
+
if style_code
|
|
350
|
+
return '</span>'
|
|
351
|
+
else
|
|
352
|
+
return ''
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
when :text
|
|
356
|
+
return ''
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# CSS stylelet
|
|
361
|
+
def css_style(colors = Screen.default_css_colors)
|
|
362
|
+
return CSSFormatter.hash_to_styles(css_styles(colors))
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# a Hash of CSS stylelet
|
|
366
|
+
def css_styles(colors = Screen.default_css_colors)
|
|
367
|
+
r = Hash.new{|h, k| h[k] = Array.new}
|
|
368
|
+
# intensity is not (yet) implemented
|
|
369
|
+
r['font-style'] << 'italic' if @italic == :on
|
|
370
|
+
r['text-decoration'] << 'underline' unless @underline == :none
|
|
371
|
+
r['text-decoration'] << 'blink' unless @blink == :off
|
|
372
|
+
case @image
|
|
373
|
+
when :positive
|
|
374
|
+
fg = @foreground
|
|
375
|
+
bg = @background
|
|
376
|
+
when :negative
|
|
377
|
+
fg = @background
|
|
378
|
+
bg = @foreground
|
|
379
|
+
end
|
|
380
|
+
fg = bg if @conceal == :on
|
|
381
|
+
r['color'] << colors[@intensity][fg] unless fg == :white
|
|
382
|
+
r['background-color'] << colors[@intensity][bg] unless bg == :black
|
|
383
|
+
return r
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
private
|
|
387
|
+
def apply_code_table!(code)
|
|
388
|
+
raise AnsiSysError, "Invalid SGR code #{code.inspect}" unless CODE.has_key?(code)
|
|
389
|
+
ivar, value = CODE[code]
|
|
390
|
+
instance_variable_set("@#{ivar}", value)
|
|
391
|
+
return self
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
CODE = {
|
|
395
|
+
1 => [:intensity, :bold],
|
|
396
|
+
2 => [:intensity, :faint],
|
|
397
|
+
3 => [:italic, :on],
|
|
398
|
+
4 => [:underline, :single],
|
|
399
|
+
5 => [:blink, :slow],
|
|
400
|
+
6 => [:blink, :rapid],
|
|
401
|
+
7 => [:image, :negative],
|
|
402
|
+
8 => [:conceal, :on],
|
|
403
|
+
21 => [:underline, :double],
|
|
404
|
+
22 => [:intensity, :normal],
|
|
405
|
+
24 => [:underline, :none],
|
|
406
|
+
25 => [:blink, :off],
|
|
407
|
+
27 => [:image, :positive],
|
|
408
|
+
28 => [:conceal, :off],
|
|
409
|
+
} # :nodoc:
|
|
410
|
+
|
|
411
|
+
COLOR = {
|
|
412
|
+
0 => :black,
|
|
413
|
+
1 => :red,
|
|
414
|
+
2 => :green,
|
|
415
|
+
3 => :yellow,
|
|
416
|
+
4 => :blue,
|
|
417
|
+
5 => :magenta,
|
|
418
|
+
6 => :cyan,
|
|
419
|
+
7 => :white,
|
|
420
|
+
} # :nodoc:
|
|
421
|
+
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
class Screen
|
|
425
|
+
# Escape sequence codes processed in this Class
|
|
426
|
+
CODE_LETTERS = %w() # :nodoc:
|
|
427
|
+
|
|
428
|
+
def self.default_foreground; :white; end
|
|
429
|
+
def self.default_background; :black; end
|
|
430
|
+
|
|
431
|
+
# a Hash of color names for each intensity
|
|
432
|
+
def self.default_css_colors(inverted = false, bright = false)
|
|
433
|
+
r = {
|
|
434
|
+
:normal => {
|
|
435
|
+
:black => 'black',
|
|
436
|
+
:red => 'maroon',
|
|
437
|
+
:green => 'green',
|
|
438
|
+
:yellow => 'olive',
|
|
439
|
+
:blue => 'navy',
|
|
440
|
+
:magenta => 'purple',
|
|
441
|
+
:cyan => 'teal',
|
|
442
|
+
:white => 'silver',
|
|
443
|
+
},
|
|
444
|
+
:bold => {
|
|
445
|
+
:black => 'gray',
|
|
446
|
+
:red => 'red',
|
|
447
|
+
:green => 'lime',
|
|
448
|
+
:yellow => 'yellow',
|
|
449
|
+
:blue => 'blue',
|
|
450
|
+
:magenta => 'fuchsia',
|
|
451
|
+
:cyan => 'cyan',
|
|
452
|
+
:white => 'white'
|
|
453
|
+
},
|
|
454
|
+
:faint => {
|
|
455
|
+
:black => 'black',
|
|
456
|
+
:red => 'maroon',
|
|
457
|
+
:green => 'green',
|
|
458
|
+
:yellow => 'olive',
|
|
459
|
+
:blue => 'navy',
|
|
460
|
+
:magenta => 'purple',
|
|
461
|
+
:cyan => 'teal',
|
|
462
|
+
:white => 'silver',
|
|
463
|
+
},
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if bright
|
|
467
|
+
r[:bold][:black] = 'black'
|
|
468
|
+
[:normal, :faint].each do |i|
|
|
469
|
+
r[i] = r[:bold]
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
if inverted
|
|
474
|
+
r.each_key do |i|
|
|
475
|
+
r[i][:black], r[i][:white] = r[i][:white], r[i][:black]
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
return r
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
# a Hash of CSS stylelet to be used in <head>
|
|
483
|
+
def self.css_styles(colors = Screen.default_css_colors, max_col = nil, max_row = nil)
|
|
484
|
+
h = {
|
|
485
|
+
'color' => [colors[:normal][:white]],
|
|
486
|
+
'background-color' => [colors[:normal][:black]],
|
|
487
|
+
'padding' => ['0.5em'],
|
|
488
|
+
}
|
|
489
|
+
h['width'] = ["#{Float(max_col)/2}em"] if max_col
|
|
490
|
+
#h['height'] = ["#{max_row}em"] if max_row # could not find appropriate unit
|
|
491
|
+
return h
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# CSS stylelet to be used in <head>.
|
|
495
|
+
# Takes the same arguments as Screen::css_styles().
|
|
496
|
+
def self.css_style(*args)
|
|
497
|
+
return "pre.screen {\n\t" + CSSFormatter.hash_to_styles(self.css_styles(*args), ";\n\t") + ";\n}\n"
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
# a Hash of keys as rows,
|
|
501
|
+
# which each value a Hash of keys columns and each value as
|
|
502
|
+
# an Array of character, its width, and associated SGR
|
|
503
|
+
attr_reader :lines
|
|
504
|
+
|
|
505
|
+
# a Screen
|
|
506
|
+
def initialize(colors = Screen.default_css_colors, max_col = nil, max_row = nil)
|
|
507
|
+
@colors = colors
|
|
508
|
+
@max_col = max_col
|
|
509
|
+
@max_row = max_row
|
|
510
|
+
@lines = Hash.new{|hash, key| hash[key] = Hash.new}
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
# CSS stylelet to be used in <head>
|
|
514
|
+
def css_style
|
|
515
|
+
self.class.css_style(@colors, @max_col, @max_row)
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
# register the _char_ at a specific location on Screen
|
|
519
|
+
def write(char, char_width, col, row, sgr)
|
|
520
|
+
@lines[Integer(row)][Integer(col)] = [char, char_width, sgr.dup]
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
# render the characters into :html or :text
|
|
524
|
+
# Class name in CSS can be specified as _css_class_.
|
|
525
|
+
# Additional stylelet can be specified as _css_style_.
|
|
526
|
+
def render(format = :html, css_class = 'screen', css_style = nil)
|
|
527
|
+
result = case format
|
|
528
|
+
when :text
|
|
529
|
+
''
|
|
530
|
+
when :html
|
|
531
|
+
%Q|<pre#{css_class ? %Q[ class="#{css_class}"] : ''}#{css_style ? %Q| style="#{css_style}"| : ''}>\n|
|
|
532
|
+
else
|
|
533
|
+
raise AnsiSysError, "Invalid format option to render: #{format.inspect}"
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
unless @lines.keys.empty?
|
|
537
|
+
prev_sgr = nil
|
|
538
|
+
max_row = @lines.keys.max
|
|
539
|
+
(1..max_row).each do |row|
|
|
540
|
+
if @lines.has_key?(row) and not @lines[row].keys.empty?
|
|
541
|
+
col = 1
|
|
542
|
+
while col <= @lines[row].keys.max
|
|
543
|
+
if @lines[row].has_key?(col) and @lines[row][col]
|
|
544
|
+
char, width, sgr = @lines[row][col]
|
|
545
|
+
if prev_sgr != sgr
|
|
546
|
+
result += prev_sgr.render(format, :postfix, @colors) if prev_sgr
|
|
547
|
+
result += sgr.render(format, :prefix, @colors)
|
|
548
|
+
prev_sgr = sgr
|
|
549
|
+
end
|
|
550
|
+
case format
|
|
551
|
+
when :text
|
|
552
|
+
result += char
|
|
553
|
+
when :html
|
|
554
|
+
result += WEBrick::HTMLUtils.escape(char)
|
|
555
|
+
end
|
|
556
|
+
col += width
|
|
557
|
+
else
|
|
558
|
+
result += ' '
|
|
559
|
+
col += 1
|
|
560
|
+
end
|
|
561
|
+
end
|
|
562
|
+
end
|
|
563
|
+
result += "\n" if row < max_row
|
|
564
|
+
end
|
|
565
|
+
result += prev_sgr.render(format, :postfix, @colors) if prev_sgr
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
result += case format
|
|
569
|
+
when :text
|
|
570
|
+
''
|
|
571
|
+
when :html
|
|
572
|
+
'</pre>'
|
|
573
|
+
end
|
|
574
|
+
return result
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
# applies self an escape sequence code that ends with _letter_ as String
|
|
578
|
+
# and with some _pars_ as Integers
|
|
579
|
+
def apply_code!(letter, *pars)
|
|
580
|
+
return self
|
|
581
|
+
end # :nodoc:
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
class Terminal
|
|
585
|
+
# Escape sequence codes processed in this Class
|
|
586
|
+
CODE_LETTERS = %w(J K S T n s u)
|
|
587
|
+
|
|
588
|
+
# _csis_ is an Array of Code Sequence Introducers
|
|
589
|
+
# which can be \e[, \x9B, or both
|
|
590
|
+
def initialize(csis = ["\x1b["])
|
|
591
|
+
@lexer = Lexer.new(csis)
|
|
592
|
+
@stream = Array.new
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
# echoes _data_, a String of characters or escape sequences
|
|
596
|
+
# to the Terminal.
|
|
597
|
+
# This method actually just buffers the echoed data.
|
|
598
|
+
def echo(data)
|
|
599
|
+
@lexer.push(data)
|
|
600
|
+
return self
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
# CSS stylelet to be used in <head>
|
|
604
|
+
def css_style(format = :html, max_col = 80, max_row = nil, colors = Screen.default_css_colors)
|
|
605
|
+
case format
|
|
606
|
+
when :html
|
|
607
|
+
Screen.css_style(colors, max_col, max_row)
|
|
608
|
+
when :text
|
|
609
|
+
''
|
|
610
|
+
end
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
# renders the echoed data as _format_ of :html or :text.
|
|
614
|
+
# _max_col_, _max_row_ can be specified as Integer.
|
|
615
|
+
# _colors_ can be Screen.default_css_colors(_inverted_, _bright_).
|
|
616
|
+
def render(format = :html, max_col = 80, max_row = nil, colors = Screen.default_css_colors, css_class = nil, css_style = nil, kcode = nil)
|
|
617
|
+
css_class ||= 'screen'
|
|
618
|
+
kcode ||= Guess.kcode(@lexer.buffer)
|
|
619
|
+
screens = populate(format, max_col, max_row, colors, kcode)
|
|
620
|
+
separator = case format
|
|
621
|
+
when :html
|
|
622
|
+
"\n"
|
|
623
|
+
when :text
|
|
624
|
+
"\n---\n"
|
|
625
|
+
end
|
|
626
|
+
return screens.map{|screen| screen.render(format, css_class, css_style)}.join(separator)
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
# applies self an escape sequence code that ends with _letter_ as String
|
|
630
|
+
# and with some _pars_ as Integers
|
|
631
|
+
def apply_code!(letter, *pars)
|
|
632
|
+
case letter
|
|
633
|
+
when 'J'
|
|
634
|
+
cur_col = @cursor.cur_col
|
|
635
|
+
cur_row = @cursor.cur_row
|
|
636
|
+
lines = @screens[-1].lines
|
|
637
|
+
if pars.empty? or 0 == pars[0]
|
|
638
|
+
rs = lines.keys.select{|r| r > cur_row}
|
|
639
|
+
cs = lines[cur_row].keys.select{|c| c >= cur_col}
|
|
640
|
+
elsif 1 == pars[0]
|
|
641
|
+
rs = lines.keys.select{|r| r < cur_row}
|
|
642
|
+
cs = lines[cur_row].keys.select{|c| c <= cur_col}
|
|
643
|
+
elsif 2 == pars[0]
|
|
644
|
+
rs = lines.keys
|
|
645
|
+
cs = []
|
|
646
|
+
@cursor.apply_code!('H', 1, 1)
|
|
647
|
+
end
|
|
648
|
+
rs.each do |r|
|
|
649
|
+
lines.delete(r)
|
|
650
|
+
end
|
|
651
|
+
cs.each do |c|
|
|
652
|
+
lines[cur_row].delete(c)
|
|
653
|
+
end
|
|
654
|
+
when 'K'
|
|
655
|
+
cur_col = @cursor.cur_col
|
|
656
|
+
cur_row = @cursor.cur_row
|
|
657
|
+
line = @screens[-1].lines[cur_row]
|
|
658
|
+
if pars.empty? or 0 == pars[0]
|
|
659
|
+
cs = line.keys.select{|c| c >= cur_col}
|
|
660
|
+
elsif 1 == pars[0]
|
|
661
|
+
cs = line.keys.select{|c| c <= cur_col}
|
|
662
|
+
elsif 2 == pars[0]
|
|
663
|
+
cs = line.keys
|
|
664
|
+
end
|
|
665
|
+
cs.each do |c|
|
|
666
|
+
line.delete(c)
|
|
667
|
+
end
|
|
668
|
+
when 'S'
|
|
669
|
+
lines = @screens[-1].lines
|
|
670
|
+
n = pars.empty? ? 1 : pars[0]
|
|
671
|
+
n.times do |l|
|
|
672
|
+
lines.delete(l)
|
|
673
|
+
end
|
|
674
|
+
rs = lines.keys.sort
|
|
675
|
+
rs.each do |r|
|
|
676
|
+
lines[r-n] = lines[r]
|
|
677
|
+
lines.delete(r)
|
|
678
|
+
end
|
|
679
|
+
@cursor.apply_code!('H', rs[-1] - n + 1, 1)
|
|
680
|
+
when 'T'
|
|
681
|
+
lines = @screens[-1].lines
|
|
682
|
+
n = pars.empty? ? 1 : pars[0]
|
|
683
|
+
rs = lines.keys.sort_by{|a| -a} # sort.reverse
|
|
684
|
+
rs.each do |r|
|
|
685
|
+
lines[r+n] = lines[r]
|
|
686
|
+
lines.delete(r)
|
|
687
|
+
end
|
|
688
|
+
@cursor.apply_code!('H', rs[-1] - n + 1, 1)
|
|
689
|
+
when 's'
|
|
690
|
+
@stored_cursor = @cursor.dup
|
|
691
|
+
when 'u'
|
|
692
|
+
@cursor = @stored_cursor.dup if @stored_cursor
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
return self
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
private
|
|
699
|
+
def populate(format = :html, max_col = 80, max_row = nil, colors = Screen.default_css_colors, kcode = nil)
|
|
700
|
+
@cursor = Cursor.new(1, 1, max_col, max_row)
|
|
701
|
+
@stored_cursor = nil
|
|
702
|
+
@screens = [Screen.new(colors, max_col, max_row)]
|
|
703
|
+
@sgr = SGR.new
|
|
704
|
+
@stream += @lexer.lex!
|
|
705
|
+
@stream.each do |type, payload|
|
|
706
|
+
case type
|
|
707
|
+
when :string
|
|
708
|
+
Characters.new(payload, @sgr).echo_on(@screens[-1], @cursor, kcode)
|
|
709
|
+
when :code
|
|
710
|
+
unless Lexer::PARAMETER_AND_LETTER =~ payload
|
|
711
|
+
raise AnsiSysError, "Invalid code: #{payload.inspect}"
|
|
712
|
+
end
|
|
713
|
+
letter = $2
|
|
714
|
+
pars = $1.split(/;/).map{|i| i.to_i}
|
|
715
|
+
applied = false
|
|
716
|
+
[@sgr, @cursor, @screens[-1], self].each do |recv|
|
|
717
|
+
if recv.class.const_get(:CODE_LETTERS).include?(letter)
|
|
718
|
+
recv.apply_code!(letter, *pars)
|
|
719
|
+
applied = true
|
|
720
|
+
end
|
|
721
|
+
end
|
|
722
|
+
raise AnsiSysError, "Invalid code or not implemented: #{payload.inspect}" unless applied
|
|
723
|
+
end
|
|
724
|
+
end
|
|
725
|
+
return @screens
|
|
726
|
+
end
|
|
727
|
+
end
|
|
728
|
+
end
|
|
729
|
+
|
|
730
|
+
if defined?(Hiki) and Hiki::Plugin == self.class
|
|
731
|
+
# a Hiki plugin method to render a file of text with ANSI escape sequences.
|
|
732
|
+
# Attached file name should be specified as _file_name_.
|
|
733
|
+
# _max_row_ can be specified.
|
|
734
|
+
# _invert_ and _bright_ can be specified to change colors.
|
|
735
|
+
# _page_ can be specified to show a file attached to another page.
|
|
736
|
+
def ansi_screen(file_name, max_col = 80, invert = false, bright = true, page = @page)
|
|
737
|
+
return '' unless file_name =~ /\.(txt|rd|rb|c|pl|py|sh|java|html|htm|css|xml|xsl|sql|yaml)\z/i
|
|
738
|
+
page_file_name = "#{page.untaint.escape}/#{file_name.untaint.escape}"
|
|
739
|
+
path = "#{@conf.cache_path}/attach/#{page_file_name}"
|
|
740
|
+
unless File.exists?(path)
|
|
741
|
+
raise PluginError, "No such file:#{page_file_name}"
|
|
742
|
+
end
|
|
743
|
+
data = File.open(path){|f| f.read}.to_euc
|
|
744
|
+
|
|
745
|
+
colors = AnsiSys::Screen.default_css_colors(invert, bright)
|
|
746
|
+
styles = AnsiSys::CSSFormatter.hash_to_styles(AnsiSys::Screen.css_styles(colors, max_col, nil), '; ')
|
|
747
|
+
|
|
748
|
+
terminal = AnsiSys::Terminal.new
|
|
749
|
+
terminal.echo(data)
|
|
750
|
+
return terminal.render(:html, max_col, nil, colors, 'screen', styles, 'e') + "\n"
|
|
751
|
+
end
|
|
752
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ansi-sys-revived
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.8.4
|
|
5
|
+
prerelease:
|
|
6
|
+
platform: ruby
|
|
7
|
+
authors:
|
|
8
|
+
- Damian Nowak
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: bin
|
|
11
|
+
cert_chain: []
|
|
12
|
+
date: 2014-05-01 00:00:00.000000000 Z
|
|
13
|
+
dependencies: []
|
|
14
|
+
description:
|
|
15
|
+
email: nowaker@pacmanvps.com
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- lib/ansisys.rb
|
|
21
|
+
homepage: https://github.com/Nowaker/ruby-ansi-sys-revived
|
|
22
|
+
licenses:
|
|
23
|
+
- GNU GPL v3
|
|
24
|
+
post_install_message:
|
|
25
|
+
rdoc_options: []
|
|
26
|
+
require_paths:
|
|
27
|
+
- lib
|
|
28
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
29
|
+
none: false
|
|
30
|
+
requirements:
|
|
31
|
+
- - ! '>='
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: 1.9.3
|
|
34
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
35
|
+
none: false
|
|
36
|
+
requirements:
|
|
37
|
+
- - ! '>='
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 1.8.11
|
|
40
|
+
requirements: []
|
|
41
|
+
rubyforge_project:
|
|
42
|
+
rubygems_version: 1.8.29
|
|
43
|
+
signing_key:
|
|
44
|
+
specification_version: 3
|
|
45
|
+
summary: A library to render texts with ANSI escape sequences. Revived from RubyForge.
|
|
46
|
+
test_files: []
|