ansi-sys 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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