asciinema-rails 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +2 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +22 -0
- data/README.md +107 -0
- data/Rakefile +2 -0
- data/asciinema-rails.gemspec +27 -0
- data/lib/asciinema-rails.rb +1 -0
- data/lib/asciinema/asciicast.rb +44 -0
- data/lib/asciinema/asciicast_frames_file_updater.rb +24 -0
- data/lib/asciinema/asciicast_snapshot_updater.rb +20 -0
- data/lib/asciinema/brush.rb +78 -0
- data/lib/asciinema/cell.rb +33 -0
- data/lib/asciinema/cursor.rb +18 -0
- data/lib/asciinema/film.rb +40 -0
- data/lib/asciinema/frame.rb +26 -0
- data/lib/asciinema/frame_diff.rb +20 -0
- data/lib/asciinema/frame_diff_list.rb +26 -0
- data/lib/asciinema/grid.rb +51 -0
- data/lib/asciinema/json_file_writer.rb +21 -0
- data/lib/asciinema/rails.rb +8 -0
- data/lib/asciinema/rails/convertor.rb +97 -0
- data/lib/asciinema/rails/engine.rb +6 -0
- data/lib/asciinema/rails/version.rb +5 -0
- data/lib/asciinema/snapshot.rb +41 -0
- data/lib/asciinema/stdout.rb +101 -0
- data/lib/asciinema/terminal.rb +65 -0
- data/spec/.rspec +2 -0
- data/spec/asciinemosh_spec.rb +63 -0
- data/spec/fixtures/sudosh-script +8 -0
- data/spec/fixtures/sudosh-time +22 -0
- data/spec/spec_helper.rb +50 -0
- data/src/terminal +0 -0
- data/src/terminal.c +307 -0
- data/vendor/assets/javscripts/asciinema-rails.js +4 -0
- data/vendor/assets/javscripts/asciinema.org/asciinema-player.js +1149 -0
- data/vendor/assets/javscripts/asciinema.org/rAF.js +32 -0
- data/vendor/assets/javscripts/asciinema.org/react-0.10.0.js +17228 -0
- data/vendor/assets/javscripts/asciinema.org/screenfull.js +143 -0
- data/vendor/assets/stylesheets/asciinema-rails.css +4 -0
- data/vendor/assets/stylesheets/asciinema.org/asciinema-player.css +1732 -0
- data/vendor/assets/stylesheets/asciinema.org/themes/solarized-dark.css +35 -0
- data/vendor/assets/stylesheets/asciinema.org/themes/solarized-light.css +35 -0
- data/vendor/assets/stylesheets/asciinema.org/themes/tango.css +35 -0
- 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,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,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
|