contrib/screenshot-generator/shoot.rb (3051B) - raw
1 require 'fileutils'
2 require 'json'
3 require 'open3'
4 require 'optparse'
5 require 'shellwords'
6 require 'tmpdir'
7 require 'yaml'
8
9 SIZE = "120x28" # terminal grid dimensions
10 IDLE = 0.3 # settling time in seconds for the terminal
11
12 class Session
13 def initialize command, env
14 ht_command = ["ht", "--size", SIZE,
15 "--subscribe", "init,output,snapshot",
16 "--", Shellwords.join(command)]
17 @stdin, @stdout, @process = Open3.popen2 env, *ht_command
18 @stdin.sync = true
19 raise "ht did not send init event" unless read_event["type"] == "init"
20 end
21
22 def write command
23 @stdin.puts JSON.generate(command)
24 end
25
26 def read_event
27 line = @stdout.gets or return nil
28 JSON.parse line
29 end
30
31 def send_keys keys
32 write :type => "sendKeys", :keys => keys
33 end
34
35 ## Wait until at least IDLE seconds has passed with no more events.
36 def settle
37 loop do
38 return unless IO.select([@stdout], nil, nil, IDLE)
39 return unless read_event
40 end
41 end
42
43 def snapshot
44 write :type => "takeSnapshot"
45 loop do
46 event = read_event
47 return event["data"] if event["type"] == "snapshot"
48 end
49 end
50
51 def expect pattern
52 settle
53 screen = snapshot
54 raise "/#{pattern.source}/ not found" unless screen["text"] =~ pattern
55 screen
56 end
57
58 def dump_screen
59 $stderr.puts "--- screen was ---"
60 $stderr.puts snapshot["text"]
61 $stderr.puts "------------------"
62 rescue
63 $stderr.puts "--- no screen ---"
64 end
65
66 def close
67 @stdin.close unless @stdin.closed?
68 @stdout.read
69 @stdout.close
70 end
71 end
72
73 options = {}
74 OptionParser.new do |parser|
75 parser.on "--sup-base PATH"
76 parser.on "--sup PATH"
77 parser.on "--scenes-yaml PATH"
78 parser.on "--out PATH"
79 end.parse! into: options
80
81 FileUtils.mkdir_p options[:out]
82
83 scenes = YAML.load_file options[:"scenes-yaml"]
84 scenes.each do |scene|
85 name = scene["name"]
86 steps = scene["steps"] || []
87
88 ## Make a fresh copy of SUP_BASE for this run of sup.
89 base = Dir.mktmpdir "sup-screenshots-#{name}-"
90 FileUtils.cp_r Dir.glob(File.join(options[:"sup-base"], "*")), base
91 FileUtils.chmod_R "u+w", base
92
93 puts "[#{name}] starting sup"
94 session = nil
95 begin
96 session = Session.new(
97 ["ruby", options[:sup], "--no-threads", "--no-initial-poll"],
98 {"SUP_BASE" => base, "SUP_LOG_LEVEL" => "debug"})
99
100 screen = session.expect /\[inbox-mode\]/
101
102 steps.each do |step|
103 keys = step["keys"]
104 puts "[#{name}] send #{keys.inspect}"
105 session.send_keys keys
106
107 screen = if step["wait"].nil?
108 session.settle
109 session.snapshot
110 else
111 session.expect Regexp.new(step["wait"])
112 end
113 end
114
115 ansi = screen["seq"]
116 ansi = ansi.gsub "\u009b", "\e[" # map CSI emitted by ht to ESC [
117 path = File.join options[:out], "#{name}.ansi"
118 File.write path, ansi
119 puts "[#{name}] wrote #{path}"
120 rescue RuntimeError => e
121 $stderr.puts "[#{name}] #{e.message}"
122 session.dump_screen if session
123 raise
124 ensure
125 session.close if session
126 end
127 end