asciinema-rails 0.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.
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