ansi-sys 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,595 @@
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
+
11
+ module AnsiSys
12
+ module VERSION #:nodoc:
13
+ MAJOR = 0
14
+ MINOR = 2
15
+ TINY = 0
16
+
17
+ STRING = [MAJOR, MINOR, TINY].join('.')
18
+ end
19
+
20
+ module CSSFormatter
21
+ def hash_to_styles(hash, separator = '; ')
22
+ unless hash.empty?
23
+ return hash.map{|e| "#{e[0]}: #{e[1].join(' ')}"}.join(separator)
24
+ else
25
+ return nil
26
+ end
27
+ end
28
+ module_function :hash_to_styles
29
+ end
30
+
31
+ class AnsiSysError < StandardError; end
32
+
33
+ class Lexer
34
+ # Control Sequence Introducer and following code
35
+ PARAMETER_AND_LETTER = /\A([\d;]*)([[:alpha:]])/o
36
+ CODE_EQUIVALENT = {
37
+ "\r" => ['B'],
38
+ "\n" => ['E'],
39
+ }
40
+
41
+ attr_reader :buffer
42
+
43
+ def initialize(csis = ["\x1b["]) # CSI can also be "\x9B"
44
+ @code_start_re = Regexp.union(*(CODE_EQUIVALENT.keys + csis))
45
+ @buffer = ''
46
+ end
47
+
48
+ def push(string)
49
+ @buffer += string
50
+ end
51
+
52
+ # returns array of tokens while deleting the tokenized part of the string
53
+ def lex!
54
+ r = Array.new
55
+ @buffer.gsub!(/(?:\r\n|\n\r)/, "\n")
56
+ while @code_start_re =~ @buffer
57
+ r << [:string, $`] unless $`.empty?
58
+ if CODE_EQUIVALENT.has_key?($&)
59
+ CODE_EQUIVALENT[$&].each do |c|
60
+ r << [:code, c]
61
+ end
62
+ @buffer = $'
63
+ else
64
+ csi = $&
65
+ residual = $'
66
+ if PARAMETER_AND_LETTER =~ residual
67
+ r << [:code, $&]
68
+ @buffer = $'
69
+ else
70
+ @buffer = csi + residual
71
+ return r
72
+ end
73
+ end
74
+ end
75
+ r << [:string, @buffer] unless @buffer.empty?
76
+ @buffer = ''
77
+ return r
78
+ end
79
+ end
80
+
81
+ class Characters
82
+ WIDTHS = {
83
+ "\t" => 8,
84
+ }
85
+
86
+ attr_reader :string
87
+ attr_reader :sgr
88
+ attr_reader :initial_cursor
89
+
90
+ def initialize(string, sgr)
91
+ @string = string
92
+ @sgr = sgr
93
+ end
94
+
95
+ def echo_on(screen, cursor)
96
+ each_char do |c|
97
+ w = width(c)
98
+ screen.write(c, w, cursor.cur_col, cursor.cur_row, @sgr.dup)
99
+ cursor.advance!(w)
100
+ end
101
+ return self
102
+ end
103
+
104
+ private
105
+ def each_char(&block)
106
+ @string.scan(/./).each do |c|
107
+ yield(c)
108
+ end
109
+ end
110
+
111
+ def width(char)
112
+ if WIDTHS.has_key?(char)
113
+ return WIDTHS[char]
114
+ end
115
+ case char.size # expecting number of bytes
116
+ when 1
117
+ return 1
118
+ else
119
+ return 2
120
+ end
121
+ end
122
+ end
123
+
124
+ class Cursor
125
+ CODE_LETTERS = %w(A B C D E F G H f)
126
+
127
+ attr_reader :cur_col
128
+ attr_reader :cur_row
129
+ attr_accessor :max_col
130
+ attr_accessor :max_row
131
+
132
+ def initialize(cur_col = 1, cur_row = 1, max_col = 80, max_row = 25)
133
+ @cur_col = cur_col
134
+ @cur_row = cur_row
135
+ @max_col = max_col
136
+ @max_row = max_row
137
+ end
138
+
139
+ def apply_code!(args)
140
+ letter = args.pop
141
+ pars = args.map do |arg|
142
+ case arg
143
+ when nil
144
+ nil
145
+ when ''
146
+ nil
147
+ else
148
+ Integer(arg)
149
+ end
150
+ end
151
+ case letter
152
+ when 'A'
153
+ @cur_row -= pars[0] ? pars[0] : 1
154
+ @cur_row = @max_row if @max_row and @cur_row > @max_row
155
+ when 'B'
156
+ @cur_row += pars[0] ? pars[0] : 1
157
+ @cur_row = @max_row if @max_row and @cur_row > @max_row
158
+ when 'C'
159
+ @cur_col += pars[0] ? pars[0] : 1
160
+ when 'D'
161
+ @cur_col -= pars[0] ? pars[0] : 1
162
+ when 'E'
163
+ @cur_row += pars[0] ? pars[0] : 1
164
+ @cur_col = 1
165
+ @max_row = @cur_row if @max_row and @cur_row > @max_row
166
+ when 'F'
167
+ @cur_row -= pars[0] ? pars[0] : 1
168
+ @cur_col = 1
169
+ @max_row = @cur_row if @max_row and @cur_row > @max_row
170
+ when 'G'
171
+ @cur_col = pars[0] ? pars[0] : 1
172
+ when 'H'
173
+ @cur_row = pars[0] ? pars[0] : 1
174
+ @cur_col = pars[1] ? pars[1] : 1
175
+ @max_row = @cur_row if @max_row and @cur_row > @max_row
176
+ when 'f'
177
+ @cur_row = pars[0] ? pars[0] : 1
178
+ @cur_col = pars[1] ? pars[1] : 1
179
+ @max_row = @cur_row if @max_row and @cur_row > @max_row
180
+ end
181
+ if @cur_row < 1
182
+ @cur_row = 1
183
+ end
184
+ if @cur_col < 1
185
+ @cur_col = 1
186
+ elsif @cur_col > @max_col
187
+ @cur_col = @max_col
188
+ end
189
+ return self
190
+ end
191
+
192
+ def advance!(width = 1)
193
+ r = nil
194
+ @cur_col += width
195
+ if @cur_col > @max_col
196
+ r = "\n"
197
+ @cur_col = 1
198
+ @cur_row += 1
199
+ end
200
+ return r
201
+ end
202
+
203
+ end
204
+
205
+ # Select Graphic Rendition
206
+ class SGR
207
+ extend CSSFormatter
208
+
209
+ CODE_LETTERS = %w(m)
210
+
211
+ attr_reader :intensity # :normal, :bold, or :faint
212
+ attr_reader :italic # :off or :on
213
+ attr_reader :underline # :none, :single, or :double
214
+ attr_reader :blink # :off, :slow, or :rapid
215
+ attr_reader :image # :positive or :negative
216
+ attr_reader :conceal # :off or :on
217
+ attr_reader :foreground # :black, :red, :green, :yellow, :blue, :magenta, :cyan, or :white
218
+ attr_reader :background # :black, :red, :green, :yellow, :blue, :magenta, :cyan, or :white
219
+
220
+ def initialize
221
+ reset!
222
+ end
223
+
224
+ def ==(other)
225
+ instance_variables.each do |ivar|
226
+ return false unless instance_eval(ivar) == other.instance_eval(ivar)
227
+ end
228
+ return true
229
+ end
230
+
231
+ def reset!
232
+ apply_code!(%w(0 m))
233
+ end
234
+
235
+ def apply_code!(args = %w(0 m))
236
+ letter = args.pop
237
+ raise AnsiSysError, "Invalid code for SGR" unless 'm' == letter
238
+ pars = args.map{|arg| arg.to_i}
239
+ pars = [0] if pars.empty?
240
+ pars.each do |code|
241
+ case code
242
+ when 0
243
+ @intensity = :normal
244
+ @italic = :off
245
+ @underline = :none
246
+ @blink = :off
247
+ @image = :positive
248
+ @conceal = :off
249
+ @foreground = :white
250
+ @background = :black
251
+ when 1..28
252
+ apply_code_table!(code)
253
+ when 30..37
254
+ @foreground = COLOR[code - 30]
255
+ @intensity = :normal
256
+ when 39
257
+ reset!
258
+ when 40..47
259
+ @background = COLOR[code - 40]
260
+ @intensity = :normal
261
+ when 49
262
+ reset!
263
+ when 90..97
264
+ @foreground = COLOR[code - 90]
265
+ @intensity = :bold
266
+ when 99
267
+ reset!
268
+ when 100..107
269
+ @background = COLOR[code - 100]
270
+ @intensity = :bold
271
+ when 109
272
+ reset!
273
+ else
274
+ raise AnsiSysError, "Invalid SGR code #{code.inspect}" unless CODE.has_key?(code)
275
+ end
276
+ end
277
+ return self
278
+ end
279
+
280
+ def render(format = :html, position = :prefix, colors = Screen.default_css_colors)
281
+ case format
282
+ when :html
283
+ case position
284
+ when :prefix
285
+ style_code = css_style(colors)
286
+ if style_code
287
+ return %Q|<span style="#{style_code}">|
288
+ else
289
+ return ''
290
+ end
291
+ when :postfix
292
+ style_code = css_style(colors)
293
+ if style_code
294
+ return '</span>'
295
+ else
296
+ return ''
297
+ end
298
+ end
299
+ when :text
300
+ return ''
301
+ end
302
+ end
303
+
304
+ def css_style(colors = Screen.default_css_colors)
305
+ return CSSFormatter.hash_to_styles(css_styles(colors))
306
+ end
307
+
308
+ def css_styles(colors = Screen.default_css_colors)
309
+ r = Hash.new{|h, k| h[k] = Array.new}
310
+ # intensity is not (yet) implemented
311
+ r['font-style'] << 'italic' if @italic == :on
312
+ r['text-decoration'] << 'underline' unless @underline == :none
313
+ r['text-decoration'] << 'blink' unless @blink == :off
314
+ case @image
315
+ when :positive
316
+ fg = @foreground
317
+ bg = @background
318
+ when :negative
319
+ fg = @background
320
+ bg = @foreground
321
+ end
322
+ fg = bg if @conceal == :on
323
+ r['color'] << colors[@intensity][fg] unless fg == :white
324
+ r['background-color'] << colors[@intensity][bg] unless bg == :black
325
+ return r
326
+ end
327
+
328
+ private
329
+ def apply_code_table!(code)
330
+ raise AnsiSysError, "Invalid SGR code #{code.inspect}" unless CODE.has_key?(code)
331
+ ivar, value = CODE[code]
332
+ instance_variable_set("@#{ivar}", value)
333
+ return self
334
+ end
335
+
336
+ CODE = {
337
+ 1 => [:intensity, :bold],
338
+ 2 => [:intensity, :faint],
339
+ 3 => [:italic, :on],
340
+ 4 => [:underline, :single],
341
+ 5 => [:blink, :slow],
342
+ 6 => [:blink, :rapid],
343
+ 7 => [:image, :negative],
344
+ 8 => [:conceal, :on],
345
+ 21 => [:underline, :double],
346
+ 22 => [:intensity, :normal],
347
+ 24 => [:underline, :none],
348
+ 25 => [:blink, :off],
349
+ 27 => [:image, :positive],
350
+ 28 => [:conceal, :off],
351
+ }
352
+
353
+ COLOR = {
354
+ 0 => :black,
355
+ 1 => :red,
356
+ 2 => :green,
357
+ 3 => :yellow,
358
+ 4 => :blue,
359
+ 5 => :magenta,
360
+ 6 => :cyan,
361
+ 7 => :white,
362
+ }
363
+
364
+ end
365
+
366
+ class Screen
367
+ CODE_LETTERS = %w()
368
+
369
+ def self.default_foreground; :white; end
370
+ def self.default_background; :black; end
371
+
372
+ # intensity -> color
373
+ def self.default_css_colors(inverted = false, bright = false)
374
+ r = {
375
+ :normal => {
376
+ :black => 'black',
377
+ :red => 'maroon',
378
+ :green => 'green',
379
+ :yellow => 'olive',
380
+ :blue => 'navy',
381
+ :magenta => 'purple',
382
+ :cyan => 'teal',
383
+ :white => 'silver',
384
+ },
385
+ :bold => {
386
+ :black => 'gray',
387
+ :red => 'red',
388
+ :green => 'lime',
389
+ :yellow => 'yellow',
390
+ :blue => 'blue',
391
+ :magenta => 'fuchsia',
392
+ :cyan => 'cyan',
393
+ :white => 'white'
394
+ },
395
+ :faint => {
396
+ :black => 'black',
397
+ :red => 'maroon',
398
+ :green => 'green',
399
+ :yellow => 'olive',
400
+ :blue => 'navy',
401
+ :magenta => 'purple',
402
+ :cyan => 'teal',
403
+ :white => 'silver',
404
+ },
405
+ }
406
+
407
+ if bright
408
+ r[:bold][:black] = 'black'
409
+ [:normal, :faint].each do |i|
410
+ r[i] = r[:bold]
411
+ end
412
+ end
413
+
414
+ if inverted
415
+ r.each_key do |i|
416
+ r[i][:black], r[i][:white] = r[i][:white], r[i][:black]
417
+ end
418
+ end
419
+
420
+ return r
421
+ end
422
+
423
+ def self.css_styles(colors = Screen.default_css_colors, max_col = nil, max_row = nil)
424
+ h = {
425
+ 'color' => [colors[:normal][:white]],
426
+ 'background-color' => [colors[:normal][:black]],
427
+ 'padding' => ['0.5em'],
428
+ }
429
+ h['width'] = ["#{Float(max_col)/2}em"] if max_col
430
+ #h['height'] = ["#{max_row}em"] if max_row # could not find appropriate unit
431
+ return h
432
+ end
433
+
434
+ def self.css_style(*args)
435
+ return "pre.screen {\n\t" + CSSFormatter.hash_to_styles(self.css_styles(*args), ";\n\t") + ";\n}\n"
436
+ end
437
+
438
+ def initialize(colors = Screen.default_css_colors, max_col = nil, max_row = nil)
439
+ @colors = colors
440
+ @max_col = max_col
441
+ @max_row = max_row
442
+ @lines = Hash.new{|hash, key| hash[key] = Hash.new}
443
+ end
444
+
445
+ def css_style
446
+ self.class.css_style(@colors, @max_col, @max_row)
447
+ end
448
+
449
+ def write(char, char_width, col, row, sgr)
450
+ @lines[Integer(row)][Integer(col)] = [char, char_width, sgr.dup]
451
+ end
452
+
453
+ def render(format = :html, css_class = 'screen', css_style = nil)
454
+ result = case format
455
+ when :text
456
+ ''
457
+ when :html
458
+ %Q|<pre#{css_class ? %Q[ class="#{css_class}"] : ''}#{css_style ? %Q| style="#{css_style}"| : ''}>\n|
459
+ else
460
+ raise AnsiSysError, "Invalid format option to render: #{format.inspect}"
461
+ end
462
+
463
+ unless @lines.keys.empty?
464
+ prev_sgr = nil
465
+ max_row = @lines.keys.max
466
+ (1..max_row).each do |row|
467
+ if @lines.has_key?(row) and not @lines[row].keys.empty?
468
+ col = 1
469
+ while col <= @lines[row].keys.max
470
+ if @lines[row].has_key?(col) and @lines[row][col]
471
+ char, width, sgr = @lines[row][col]
472
+ if prev_sgr != sgr
473
+ result += prev_sgr.render(format, :postfix, @colors) if prev_sgr
474
+ result += sgr.render(format, :prefix, @colors)
475
+ prev_sgr = sgr
476
+ end
477
+ case format
478
+ when :text
479
+ result += char
480
+ when :html
481
+ result += WEBrick::HTMLUtils.escape(char)
482
+ end
483
+ col += width
484
+ else
485
+ result += ' '
486
+ col += 1
487
+ end
488
+ end
489
+ end
490
+ result += "\n" if row < max_row
491
+ end
492
+ result += prev_sgr.render(format, :postfix, @colors) if prev_sgr
493
+ end
494
+
495
+ result += case format
496
+ when :text
497
+ ''
498
+ when :html
499
+ '</pre>'
500
+ end
501
+ return result
502
+ end
503
+
504
+ def apply_code!(args) # TODO - some codes
505
+ return self
506
+ end
507
+ end
508
+
509
+ class Terminal
510
+ CODE_LETTERS = [] # TODO - make a new screen
511
+
512
+ def initialize(csis = ["\x1b["])
513
+ @lexer = Lexer.new(csis)
514
+ @stream = Array.new
515
+ end
516
+
517
+ def echo(data)
518
+ @lexer.push(data)
519
+ return self
520
+ end
521
+
522
+ def css_style(format = :html, max_col = 80, max_row = nil, colors = Screen.default_css_colors)
523
+ case format
524
+ when :html
525
+ Screen.css_style(colors, max_col, max_row)
526
+ when :text
527
+ ''
528
+ end
529
+ end
530
+
531
+ def render(format = :html, max_col = 80, max_row = nil, colors = Screen.default_css_colors, css_class = 'screen', css_style = nil)
532
+ screens = populate(format, max_col, max_row, colors)
533
+ separator = case format
534
+ when :html
535
+ "\n"
536
+ when :text
537
+ "\n---\n"
538
+ end
539
+ return screens.map{|screen| screen.render(format, css_class, css_style)}.join(separator)
540
+ end
541
+
542
+ private
543
+ def populate(format = :html, max_col = 80, max_row = nil, colors = Screen.default_css_colors)
544
+ cursor = Cursor.new(1, 1, max_col, max_row)
545
+ screens = [Screen.new(colors, max_col, max_row)]
546
+ sgr = SGR.new
547
+ @stream += @lexer.lex!
548
+ @stream.each do |type, payload|
549
+ case type
550
+ when :string
551
+ Characters.new(payload, sgr).echo_on(screens[-1], cursor)
552
+ when :code
553
+ unless Lexer::PARAMETER_AND_LETTER =~ payload
554
+ raise AnsiSysError, "Invalid code: #{payload.inspect}"
555
+ end
556
+ parameters = $1
557
+ letter = $2
558
+ args = parameters.split(/;/) + [letter]
559
+ applied = false
560
+ [sgr, cursor, screens[-1], self].each do |recv|
561
+ if recv.class.const_get(:CODE_LETTERS).include?(letter)
562
+ recv.apply_code!(args)
563
+ applied = true
564
+ end
565
+ end
566
+ raise AnsiSysError, "Invalid code or not implemented: #{payload.inspect}" unless applied
567
+ end
568
+ end
569
+ return screens
570
+ end
571
+
572
+ def apply_code!(args) # TODO - make a new screen
573
+ return self
574
+ end
575
+ end
576
+ end
577
+
578
+ if defined?(Hiki) and Hiki::Plugin == self.class
579
+ def ansi_screen(file_name, max_row = 80, invert = false, bright = true, page = @page)
580
+ return '' unless file_name =~ /\.(txt|rd|rb|c|pl|py|sh|java|html|htm|css|xml|xsl|sql|yaml)\z/i
581
+ page_file_name = "#{page.untaint.escape}/#{file_name.untaint.escape}"
582
+ path = "#{@conf.cache_path}/attach/#{page_file_name}"
583
+ unless File.exists?(path)
584
+ raise PluginError, "No such file:#{page_file_name}"
585
+ end
586
+ data = File.open(path){|f| f.read}
587
+
588
+ colors = AnsiSys::Screen.default_css_colors(invert, bright)
589
+ styles = AnsiSys::CSSFormatter.hash_to_styles(AnsiSys::Screen.css_styles(colors, max_row, nil), '; ')
590
+
591
+ terminal = AnsiSys::Terminal.new
592
+ terminal.echo(data)
593
+ return terminal.render(:html, max_row, nil, colors, 'screen', styles) + "\n"
594
+ end
595
+ end