asciinema-rails 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +9 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +107 -0
  7. data/Rakefile +2 -0
  8. data/asciinema-rails.gemspec +27 -0
  9. data/lib/asciinema-rails.rb +1 -0
  10. data/lib/asciinema/asciicast.rb +44 -0
  11. data/lib/asciinema/asciicast_frames_file_updater.rb +24 -0
  12. data/lib/asciinema/asciicast_snapshot_updater.rb +20 -0
  13. data/lib/asciinema/brush.rb +78 -0
  14. data/lib/asciinema/cell.rb +33 -0
  15. data/lib/asciinema/cursor.rb +18 -0
  16. data/lib/asciinema/film.rb +40 -0
  17. data/lib/asciinema/frame.rb +26 -0
  18. data/lib/asciinema/frame_diff.rb +20 -0
  19. data/lib/asciinema/frame_diff_list.rb +26 -0
  20. data/lib/asciinema/grid.rb +51 -0
  21. data/lib/asciinema/json_file_writer.rb +21 -0
  22. data/lib/asciinema/rails.rb +8 -0
  23. data/lib/asciinema/rails/convertor.rb +97 -0
  24. data/lib/asciinema/rails/engine.rb +6 -0
  25. data/lib/asciinema/rails/version.rb +5 -0
  26. data/lib/asciinema/snapshot.rb +41 -0
  27. data/lib/asciinema/stdout.rb +101 -0
  28. data/lib/asciinema/terminal.rb +65 -0
  29. data/spec/.rspec +2 -0
  30. data/spec/asciinemosh_spec.rb +63 -0
  31. data/spec/fixtures/sudosh-script +8 -0
  32. data/spec/fixtures/sudosh-time +22 -0
  33. data/spec/spec_helper.rb +50 -0
  34. data/src/terminal +0 -0
  35. data/src/terminal.c +307 -0
  36. data/vendor/assets/javscripts/asciinema-rails.js +4 -0
  37. data/vendor/assets/javscripts/asciinema.org/asciinema-player.js +1149 -0
  38. data/vendor/assets/javscripts/asciinema.org/rAF.js +32 -0
  39. data/vendor/assets/javscripts/asciinema.org/react-0.10.0.js +17228 -0
  40. data/vendor/assets/javscripts/asciinema.org/screenfull.js +143 -0
  41. data/vendor/assets/stylesheets/asciinema-rails.css +4 -0
  42. data/vendor/assets/stylesheets/asciinema.org/asciinema-player.css +1732 -0
  43. data/vendor/assets/stylesheets/asciinema.org/themes/solarized-dark.css +35 -0
  44. data/vendor/assets/stylesheets/asciinema.org/themes/solarized-light.css +35 -0
  45. data/vendor/assets/stylesheets/asciinema.org/themes/tango.css +35 -0
  46. metadata +192 -0
@@ -0,0 +1,26 @@
1
+ require 'asciinema/frame_diff'
2
+
3
+ class Frame
4
+
5
+ attr_reader :snapshot, :cursor
6
+
7
+ def initialize(snapshot, cursor)
8
+ @snapshot = snapshot
9
+ @cursor = cursor
10
+ end
11
+
12
+ def diff(other)
13
+ FrameDiff.new(snapshot_diff(other), cursor_diff(other))
14
+ end
15
+
16
+ private
17
+
18
+ def snapshot_diff(other)
19
+ snapshot.diff(other && other.snapshot)
20
+ end
21
+
22
+ def cursor_diff(other)
23
+ cursor.diff(other && other.cursor)
24
+ end
25
+
26
+ end
@@ -0,0 +1,20 @@
1
+ class FrameDiff
2
+
3
+ def initialize(line_changes, cursor_changes)
4
+ @line_changes = line_changes
5
+ @cursor_changes = cursor_changes
6
+ end
7
+
8
+ def as_json(*)
9
+ json = {}
10
+ json[:lines] = line_changes unless line_changes.blank?
11
+ json[:cursor] = cursor_changes unless cursor_changes.blank?
12
+
13
+ json
14
+ end
15
+
16
+ private
17
+
18
+ attr_reader :line_changes, :cursor_changes
19
+
20
+ end
@@ -0,0 +1,26 @@
1
+ require 'active_support/all'
2
+
3
+ class FrameDiffList
4
+ include Enumerable
5
+
6
+ delegate :each, :to => :frame_diffs
7
+
8
+ def initialize(frames)
9
+ @frames = frames
10
+ end
11
+
12
+ private
13
+
14
+ attr_reader :frames
15
+
16
+ def frame_diffs
17
+ previous_frame = nil
18
+
19
+ frames.map { |delay, frame|
20
+ diff = frame.diff(previous_frame)
21
+ previous_frame = frame
22
+ [delay, diff]
23
+ }
24
+ end
25
+
26
+ end
@@ -0,0 +1,51 @@
1
+ class Grid
2
+ require 'active_support/core_ext/enumerable'
3
+
4
+ attr_reader :width, :height, :lines
5
+
6
+ def initialize(lines)
7
+ @lines = lines
8
+ @width = lines.first && lines.first.sum(&:size) || 0
9
+ @height = lines.size
10
+ end
11
+
12
+ def crop(x, y, width, height)
13
+ cropped_lines = lines[y...y+height].map { |line| crop_line(line, x, width) }
14
+
15
+ self.class.new(cropped_lines)
16
+ end
17
+
18
+ def diff(other)
19
+ (0...height).each_with_object({}) do |y, diff|
20
+ if other.nil? || other.lines[y] != lines[y]
21
+ diff[y] = lines[y]
22
+ end
23
+ end
24
+ end
25
+
26
+ def as_json(*)
27
+ lines.as_json
28
+ end
29
+
30
+ private
31
+
32
+ def crop_line(line, x, width)
33
+ n = 0
34
+ cells = []
35
+
36
+ line.each do |cell|
37
+ if n <= x && x < n + cell.size
38
+ cells << cell[x-n...x-n+width]
39
+ elsif x < n && x + width >= n + cell.size
40
+ cells << cell
41
+ elsif n < x + width && x + width < n + cell.size
42
+ cells << cell[0...x+width-n]
43
+ end
44
+
45
+ n += cell.size
46
+ end
47
+
48
+ cells
49
+ end
50
+
51
+ end
@@ -0,0 +1,21 @@
1
+ class JsonFileWriter
2
+
3
+ def write_enumerable(file, array)
4
+ first = true
5
+ file << '['
6
+
7
+ array.each do |item|
8
+ if first
9
+ first = false
10
+ else
11
+ file << ','
12
+ end
13
+
14
+ file << item.to_json
15
+ end
16
+
17
+ file << ']'
18
+ file.close
19
+ end
20
+
21
+ end
@@ -0,0 +1,8 @@
1
+ require "asciinema/rails/engine"
2
+ require "asciinema/rails/version"
3
+ require "asciinema/rails/convertor"
4
+
5
+ module Asciinema
6
+ module Rails
7
+ end
8
+ end
@@ -0,0 +1,97 @@
1
+ require 'json'
2
+ require 'asciinema/asciicast'
3
+ require 'asciinema/asciicast_frames_file_updater'
4
+ require 'asciinema/asciicast_snapshot_updater'
5
+
6
+
7
+
8
+ module Asciinema
9
+ module Rails
10
+ class Convertor
11
+
12
+ # Generate an asciicast from Sudosh log files.
13
+
14
+ # *Params*:
15
+ # - sudosh_timing_file_path: path to the Sudosh timing file
16
+ # - sudosh_script_file_path: path to the Sudosh script file
17
+ # - options: Additional options hash:
18
+ # * +:original_terminal_cols+: width of the Sudosh session terminal
19
+ # * +:original_terminal_rows+: height of the Sudosh session terminal
20
+ # * +:asciicast_path+: file to write output to
21
+ # *Returns*:
22
+ # - A File object pointing to the generated asciicast.
23
+
24
+ def self.sudosh_to_asciicast(sudosh_timing_file_path, sudosh_script_file_path, options = {})
25
+ original_terminal_cols = options[:original_terminal_cols] || 180
26
+ original_terminal_rows = options[:original_terminal_rows] || 43
27
+ asciicast_path = options[:asciicast_path]
28
+
29
+ timings = []
30
+ byte_offsets = []
31
+
32
+ # Split the sudosh timing file into individual time splits and data byte offsets.
33
+ # When paired with the Sudosh data file, each time split can be though of as an animation 'frame'
34
+ File.open(sudosh_timing_file_path) do |f|
35
+ f.each_line.each do |line|
36
+ split = line.split(' ')
37
+ timings << split[0].to_f
38
+ byte_offsets << split[1].to_i
39
+ end
40
+ end
41
+ duration = timings.inject(:+) ## add all of the time splits to get the total duration
42
+
43
+ # Split the script file into segments as defined by the byte offsets in the timing file.
44
+ # TODO: Write stdout directly to file rather than into memory first.
45
+ stdout = []
46
+ File.open(sudosh_script_file_path, 'rb') do |file|
47
+ until file.eof?
48
+ timestamp = '%.5f' % timings.shift.to_f
49
+ terminal_chunk = file.read(byte_offsets.shift)
50
+ stdout << [Float(timestamp), terminal_chunk]
51
+ end
52
+ end
53
+
54
+ json = {version: 1, width: original_terminal_cols, height: original_terminal_rows, duration: duration}
55
+ json[:stdout] = stdout
56
+
57
+ self.to_file(JSON.pretty_generate(json), asciicast_path)
58
+
59
+ end
60
+
61
+ # Generate a playback file from an asciicast.
62
+
63
+ # *Params*:
64
+ # - asciicast_path: path to the asciicast file
65
+ # - options: Additional options hash:
66
+ # * +:playback_path+: file to write the playback data to
67
+ # *Returns*:
68
+ # - An array containing:
69
+ # * the playback file content as a string
70
+ # * a snapshot string
71
+ # * a hash containing the original width (cols), height (rows) and duration of the asciicast.
72
+
73
+ def self.asciicast_to_playback(asciicast_path, options = {})
74
+ playback_path = options[:playback_path] # playback data will be written here
75
+
76
+ json = JSON.parse(File.read(asciicast_path)) # load the asciicast data into a hash
77
+ asciicast = Asciicast.new(json['width'], json['height'], json['duration'], asciicast_path) # create an Asciicast object
78
+ AsciicastFramesFileUpdater.new.update(asciicast, playback_path) # set playback data in Asciicast object, write to file also
79
+ AsciicastSnapshotUpdater.new.update(asciicast) # set snapshot data in Asciicast object.
80
+
81
+ [asciicast.stdout_frames, ActiveSupport::JSON.encode(asciicast.snapshot), {width: json['width'], height: json['height'], duration: json['duration']} ]
82
+
83
+ end
84
+
85
+ private
86
+
87
+ def self.to_file(content, outfile_location=nil)
88
+ out_file = outfile_location.present? ? File.new(outfile_location) : Tempfile.new('outfile')
89
+ File.open(out_file, "w") do |f|
90
+ f.write(content)
91
+ end
92
+ out_file
93
+ end
94
+ end
95
+
96
+ end
97
+ end
@@ -0,0 +1,6 @@
1
+ module Asciinema
2
+ module Rails
3
+ class Engine < ::Rails::Engine
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module Asciinema
2
+ module Rails
3
+ VERSION = "0.1.1"
4
+ end
5
+ end
@@ -0,0 +1,41 @@
1
+ require 'asciinema/cell'
2
+ require 'asciinema/brush'
3
+
4
+ class Snapshot < Grid
5
+
6
+ def self.build(data)
7
+ data = data.map { |cells|
8
+ cells.map { |cell|
9
+ Cell.new(cell[0], Brush.new(cell[1]))
10
+ }
11
+ }
12
+
13
+ new(data)
14
+ end
15
+
16
+ def thumbnail(w, h)
17
+ x = 0
18
+ y = height - h - trailing_empty_lines
19
+ y = 0 if y < 0
20
+
21
+ crop(x, y, w, h)
22
+ end
23
+
24
+ private
25
+
26
+ def trailing_empty_lines
27
+ n = 0
28
+
29
+ (height - 1).downto(0) do |y|
30
+ break unless line_empty?(y)
31
+ n += 1
32
+ end
33
+
34
+ n
35
+ end
36
+
37
+ def line_empty?(y)
38
+ lines[y].empty? || lines[y].all? { |cell| cell.empty? }
39
+ end
40
+
41
+ end
@@ -0,0 +1,101 @@
1
+ require 'open-uri'
2
+ require 'oj'
3
+
4
+ class Stdout
5
+ include Enumerable
6
+
7
+ class SingleFile < self
8
+ attr_reader :path
9
+
10
+ def initialize(path)
11
+ @path = path
12
+ end
13
+
14
+ def each(&blk)
15
+ open(path, 'r') do |f|
16
+ Oj.sc_parse(FrameIterator.new(blk), f)
17
+ end
18
+ end
19
+
20
+ class FrameIterator < ::Oj::ScHandler
21
+
22
+ def initialize(callback)
23
+ @callback = callback
24
+ end
25
+
26
+ def array_start
27
+ if @top # we're already inside top level array
28
+ [] # <- this will hold pair [delay, data]
29
+ else
30
+ @top = []
31
+ end
32
+ end
33
+
34
+ def array_append(a, v)
35
+ if a.equal?(@top)
36
+ @callback.call(*v)
37
+ else
38
+ a << v
39
+ end
40
+ end
41
+
42
+ end
43
+
44
+ end
45
+
46
+ class MultiFile < self
47
+ attr_reader :data_path, :timing_path
48
+
49
+ def initialize(data_path, timing_path)
50
+ @data_path = data_path
51
+ @timing_path = timing_path
52
+ end
53
+
54
+ def each
55
+ File.open(data_path, 'rb') do |file|
56
+ File.foreach(timing_path) do |line|
57
+ yield(*delay_and_data_for_line(file, line))
58
+ end
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def delay_and_data_for_line(file, line)
65
+ delay, size = TimingParser.parse_line(line)
66
+ data = file.read(size).to_s.force_encoding('utf-8')
67
+
68
+ [delay, data]
69
+ end
70
+
71
+ end
72
+
73
+ class Buffered < self
74
+ MIN_FRAME_LENGTH = 1.0 / 60
75
+
76
+ attr_reader :stdout
77
+
78
+ def initialize(stdout)
79
+ @stdout = stdout
80
+ end
81
+
82
+ def each
83
+ buffered_delay, buffered_data = 0.0, []
84
+
85
+ stdout.each do |delay, data|
86
+ if buffered_delay + delay < MIN_FRAME_LENGTH || buffered_data.empty?
87
+ buffered_delay += delay
88
+ buffered_data << data
89
+ else
90
+ yield(buffered_delay, buffered_data.join)
91
+ buffered_delay = delay
92
+ buffered_data = [data]
93
+ end
94
+ end
95
+
96
+ yield(buffered_delay, buffered_data.join) unless buffered_data.empty?
97
+ end
98
+
99
+ end
100
+
101
+ end
@@ -0,0 +1,65 @@
1
+ require 'open3'
2
+ require 'yajl'
3
+ require 'asciinema/grid'
4
+ require 'asciinema/snapshot'
5
+ require 'asciinema/cursor'
6
+
7
+ class Terminal
8
+
9
+ # BINARY_PATH = (File.dirname(__FILE__) + "/../../bin/" + "terminal").to_s
10
+ BINARY_PATH = 'terminal'
11
+
12
+ def initialize(width, height)
13
+ @process = Process.new("#{BINARY_PATH} #{width} #{height}")
14
+ end
15
+
16
+ def feed(data)
17
+ process.write("d\n#{data.bytesize}\n")
18
+ process.write(data)
19
+ end
20
+
21
+ def snapshot
22
+ process.write("p\n")
23
+ lines = Yajl::Parser.new.parse(process.read_line)
24
+
25
+ Snapshot.build(lines)
26
+ end
27
+
28
+ def cursor
29
+ process.write("c\n")
30
+ c = Yajl::Parser.new.parse(process.read_line)
31
+
32
+ Cursor.new(c['x'], c['y'], c['visible'])
33
+ end
34
+
35
+ def release
36
+ process.stop
37
+ end
38
+
39
+ private
40
+
41
+ attr_reader :process
42
+
43
+ class Process
44
+
45
+ def initialize(command)
46
+ @stdin, @stdout, @thread = Open3.popen2(command)
47
+ end
48
+
49
+ def write(data)
50
+ raise "terminal died" unless @thread.alive?
51
+ @stdin.write(data)
52
+ end
53
+
54
+ def read_line
55
+ raise "terminal died" unless @thread.alive?
56
+ @stdout.readline.strip
57
+ end
58
+
59
+ def stop
60
+ @stdin.close
61
+ end
62
+
63
+ end
64
+
65
+ end