terminal 0.2 → 0.3.beta1
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.
- checksums.yaml +4 -4
- data/.rspec +2 -0
- data/Gemfile.lock +11 -5
- data/app/assets/stylesheets/terminal.css +256 -0
- data/bin/terminal +1 -0
- data/lib/terminal.rb +3 -4
- data/lib/terminal/engine.rb +4 -0
- data/lib/terminal/preview.rb +34 -3
- data/lib/terminal/renderer.rb +111 -162
- data/lib/terminal/screen.rb +282 -0
- data/lib/terminal/templates/preview.html.erb +46 -89
- data/lib/terminal/version.rb +1 -1
- data/script/benchmark +20 -0
- data/script/buildbox/benchmark.sh +8 -0
- data/script/buildbox/profile_methods.sh +8 -0
- data/script/buildbox/specs.sh +8 -0
- data/script/profile_methods +21 -0
- data/spec/fixtures/curl.sh.raw +1 -1
- data/spec/fixtures/curl.sh.rendered +2 -2
- data/spec/fixtures/homer.sh.rendered +18 -18
- data/spec/fixtures/pikachu.sh.raw +47 -0
- data/spec/fixtures/pikachu.sh.rendered +46 -0
- data/spec/terminal/renderer_spec.rb +129 -22
- data/spec/terminal/screen_spec.rb +77 -0
- data/terminal.gemspec +5 -3
- metadata +50 -10
- data/lib/terminal/color.rb +0 -7
- data/lib/terminal/node.rb +0 -15
- data/lib/terminal/reset.rb +0 -16
@@ -0,0 +1,282 @@
|
|
1
|
+
# A fake terminal screen. Usage is like this:
|
2
|
+
#
|
3
|
+
# screen = Screen.new
|
4
|
+
# screen.x = 10
|
5
|
+
# screen.y = 10
|
6
|
+
# screen.write('h')
|
7
|
+
#
|
8
|
+
# Will essentially create a 10 x 10 grid with empty characters, and at the
|
9
|
+
# 10,10 spot element, there will be a 'h'. Co-ordinates start at 0,0
|
10
|
+
#
|
11
|
+
# It also supports writing colors. So if you were to change the color like so:
|
12
|
+
#
|
13
|
+
# screen.color("42")
|
14
|
+
#
|
15
|
+
# Ever new character that you write, will be stored with that color
|
16
|
+
# information.
|
17
|
+
#
|
18
|
+
# When turned into a string, the screen class creates ANSI escape characters,
|
19
|
+
# that the renderer class gsubs out.
|
20
|
+
#
|
21
|
+
# \e[fg32;bg42;
|
22
|
+
# \e[fgi91;;
|
23
|
+
# \e[fgx102;bgx102;
|
24
|
+
# \e[0m
|
25
|
+
#
|
26
|
+
# Are some of the examples of the escape sequences that this will render.
|
27
|
+
|
28
|
+
module Terminal
|
29
|
+
class Screen
|
30
|
+
class Node < Struct.new(:blob, :fg, :bg)
|
31
|
+
def ==(value)
|
32
|
+
blob == value
|
33
|
+
end
|
34
|
+
|
35
|
+
# Every node has a style, a foreground style, and a background
|
36
|
+
# style. This method returns what essentially becomes the escape
|
37
|
+
# sequence:
|
38
|
+
#
|
39
|
+
# \e[fg;bg;
|
40
|
+
#
|
41
|
+
# As the screen is turned into a string, the style is used to compare
|
42
|
+
# whether or not a new escape sequence is required.
|
43
|
+
def style
|
44
|
+
if fg || bg
|
45
|
+
"#{fg};#{bg};"
|
46
|
+
else
|
47
|
+
nil
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_s
|
52
|
+
blob
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
END_OF_LINE = :end_of_line
|
57
|
+
START_OF_LINE = :start_of_line
|
58
|
+
EMPTY = Node.new(" ")
|
59
|
+
|
60
|
+
attr_reader :x, :y
|
61
|
+
|
62
|
+
def initialize
|
63
|
+
@x = 0
|
64
|
+
@y = 0
|
65
|
+
@screen = []
|
66
|
+
@fg = nil
|
67
|
+
end
|
68
|
+
|
69
|
+
def write(character)
|
70
|
+
# Expand the screen if we need to
|
71
|
+
((@y + 1) - @screen.length).times do
|
72
|
+
@screen << []
|
73
|
+
end
|
74
|
+
|
75
|
+
line = @screen[@y]
|
76
|
+
line_length = line.length
|
77
|
+
|
78
|
+
# Write empty slots until we reach the line
|
79
|
+
(@x - line_length).times do |i|
|
80
|
+
line[line_length + i] = EMPTY
|
81
|
+
end
|
82
|
+
|
83
|
+
# Write the character to the slot
|
84
|
+
line[@x] = Node.new(character, @fg, @bg)
|
85
|
+
end
|
86
|
+
|
87
|
+
def <<(character)
|
88
|
+
write(character)
|
89
|
+
@x += 1
|
90
|
+
character
|
91
|
+
end
|
92
|
+
|
93
|
+
def x=(value)
|
94
|
+
@x = value > 0 ? value : 0
|
95
|
+
end
|
96
|
+
|
97
|
+
def y=(value)
|
98
|
+
@y = value > 0 ? value : 0
|
99
|
+
end
|
100
|
+
|
101
|
+
def clear(y, x_start, x_end)
|
102
|
+
line = @screen[y]
|
103
|
+
|
104
|
+
# If the line isn't there, we can't clean it.
|
105
|
+
return if line.nil?
|
106
|
+
|
107
|
+
x_start = 0 if x_start == START_OF_LINE
|
108
|
+
x_end = line.length - 1 if x_end == END_OF_LINE
|
109
|
+
|
110
|
+
if x_start == START_OF_LINE && x_end == END_OF_LINE
|
111
|
+
@screen[y] = []
|
112
|
+
else
|
113
|
+
line.fill(EMPTY, x_start..x_end)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Changes the current foreground color that all new characters
|
118
|
+
# will be written with.
|
119
|
+
def color(color_code)
|
120
|
+
# Reset all styles
|
121
|
+
if color_code == "0"
|
122
|
+
@fg = nil
|
123
|
+
@bg = nil
|
124
|
+
return color_code
|
125
|
+
end
|
126
|
+
|
127
|
+
# Reset foreground color only
|
128
|
+
if color_code == "39"
|
129
|
+
@fg = nil
|
130
|
+
return color_code
|
131
|
+
end
|
132
|
+
|
133
|
+
# Reset background color only
|
134
|
+
if color_code == "49"
|
135
|
+
@bg = nil
|
136
|
+
return color_code
|
137
|
+
end
|
138
|
+
|
139
|
+
colors = color_code.to_s.split(";")
|
140
|
+
|
141
|
+
# Extended set foreground x-term color
|
142
|
+
if colors[0] == "38" && colors[1] == "5"
|
143
|
+
return @fg = "fgx#{colors[2]}"
|
144
|
+
end
|
145
|
+
|
146
|
+
# Extended set background x-term color
|
147
|
+
if colors[0] == "48" && colors[1] == "5"
|
148
|
+
return @bg = "bgx#{colors[2]}"
|
149
|
+
end
|
150
|
+
|
151
|
+
# If multiple colors are defined, i.e. \e[30;42m\e
|
152
|
+
# then loop through each one, and assign it to @fg
|
153
|
+
# or @bg
|
154
|
+
colors.each do |cc|
|
155
|
+
# If the number is between 30–37, then it's a foreground color,
|
156
|
+
# if it's 40–47, then it's a background color. 90-97 is like the regular
|
157
|
+
# foreground 30-37, but it's high intensity
|
158
|
+
#
|
159
|
+
# I don't use ranges and a select because I've found that to be rather
|
160
|
+
# slow.
|
161
|
+
c_integer = cc.to_i
|
162
|
+
if c_integer >= 30 && c_integer <= 37
|
163
|
+
@fg = "fg#{cc}"
|
164
|
+
elsif c_integer >= 40 && c_integer <= 47
|
165
|
+
@bg = "bg#{cc}"
|
166
|
+
elsif c_integer >= 90 && c_integer <= 97
|
167
|
+
@fg = "fgi#{cc}"
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def up(value = nil)
|
173
|
+
self.y -= parse_integer(value)
|
174
|
+
end
|
175
|
+
|
176
|
+
def down(value = nil)
|
177
|
+
new_y = @y + parse_integer(value)
|
178
|
+
|
179
|
+
# Only jump down if the line exists
|
180
|
+
if @screen[new_y]
|
181
|
+
self.y = new_y
|
182
|
+
else
|
183
|
+
false
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def backward(value = nil)
|
188
|
+
self.x -= parse_integer(value)
|
189
|
+
end
|
190
|
+
|
191
|
+
def foward(value = nil)
|
192
|
+
self.x += parse_integer(value)
|
193
|
+
end
|
194
|
+
|
195
|
+
def to_a
|
196
|
+
@screen.to_a.map { |chars| chars.map(&:to_s) }
|
197
|
+
end
|
198
|
+
|
199
|
+
# Renders each node to a string. This looks at each node, and then inserts
|
200
|
+
# escape characters that will be gsubed into <span> elements.
|
201
|
+
#
|
202
|
+
# ANSI codes generally span across lines. So if you \e[12m\n\nhello, the hello will
|
203
|
+
# inhert the styles of \e[12m. This doesn't work so great in HTML, especially if you
|
204
|
+
# wrap divs around each line, so this method also copies any styles that are left open
|
205
|
+
# at the end of a line, to the begining of new lines, so you end up with something like this:
|
206
|
+
#
|
207
|
+
# \e[12m\n\e[12m\n\e[12mhello
|
208
|
+
#
|
209
|
+
# It also attempts to only insert escapes that are required. Given the following:
|
210
|
+
#
|
211
|
+
# \e[12mh\e[12me\e[12ml\e[12ml\e[12mo\e[0m
|
212
|
+
#
|
213
|
+
# A general purpose ANSI renderer will convert it to:
|
214
|
+
#
|
215
|
+
# <span class="c12">h<span class="c12">e<span class="c12">l<span class="c12">l<span class="c12">o</span></span></span></span>
|
216
|
+
#
|
217
|
+
# But ours is smart, and tries to do stuff like this:
|
218
|
+
#
|
219
|
+
# <span class="c12">hello</span>
|
220
|
+
def to_s
|
221
|
+
last_line_index = @screen.length - 1
|
222
|
+
buffer = []
|
223
|
+
|
224
|
+
@screen.each_with_index do |line, line_index|
|
225
|
+
previous = nil
|
226
|
+
|
227
|
+
# Keep track of every open style we have, so we know
|
228
|
+
# that we need to close any open ones at the end.
|
229
|
+
open_styles = 0
|
230
|
+
|
231
|
+
line.each do |node|
|
232
|
+
# If there is no previous node, and the current node has a color
|
233
|
+
# (first node in a line) then add the escape character.
|
234
|
+
if !previous && node.style
|
235
|
+
buffer << "\e[#{node.style}m"
|
236
|
+
|
237
|
+
# Increment the open style counter
|
238
|
+
open_styles += 1
|
239
|
+
|
240
|
+
# If we have a previous node, and the last node's style doesn't
|
241
|
+
# match this nodes, then we start a new escape character.
|
242
|
+
elsif previous && previous.style != node.style
|
243
|
+
# If the new node has no style, that means that all the styles
|
244
|
+
# have been closed.
|
245
|
+
if !node.style
|
246
|
+
# Add our reset escape character
|
247
|
+
buffer << "\e[0m"
|
248
|
+
|
249
|
+
# Decrement the open style counter
|
250
|
+
open_styles -= 1
|
251
|
+
else
|
252
|
+
buffer << "\e[#{node.style}m"
|
253
|
+
|
254
|
+
# Increment the open style counter
|
255
|
+
open_styles += 1
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
# Add the nodes blob to te buffer
|
260
|
+
buffer << node.blob
|
261
|
+
|
262
|
+
# Set this node as being the previous node
|
263
|
+
previous = node
|
264
|
+
end
|
265
|
+
|
266
|
+
# Be sure to close off any open styles for this line
|
267
|
+
open_styles.times { buffer << "\e[0m" }
|
268
|
+
|
269
|
+
# Add a new line as long as this line isn't the last
|
270
|
+
buffer << "\n" if line_index != last_line_index
|
271
|
+
end
|
272
|
+
|
273
|
+
buffer.join("")
|
274
|
+
end
|
275
|
+
|
276
|
+
private
|
277
|
+
|
278
|
+
def parse_integer(value)
|
279
|
+
value.nil? || value == "" ? 1 : value.to_i
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
@@ -1,99 +1,56 @@
|
|
1
1
|
<!DOCTYPE html>
|
2
2
|
<html>
|
3
3
|
<head>
|
4
|
+
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
|
5
|
+
<link href="<%= asset_path("stylesheets/terminal.css") %>" media="screen" rel="stylesheet" />
|
4
6
|
<style>
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
}
|
33
|
-
|
34
|
-
/* intense green */
|
35
|
-
.c1 {
|
36
|
-
color: #5ef765;
|
37
|
-
}
|
38
|
-
|
39
|
-
/* red */
|
40
|
-
.c31 {
|
41
|
-
color: #e10c02;
|
42
|
-
}
|
43
|
-
|
44
|
-
/* blue */
|
45
|
-
.c34 {
|
46
|
-
color: #8db7e0;
|
47
|
-
}
|
48
|
-
|
49
|
-
/* magent */
|
50
|
-
.c35 {
|
51
|
-
color: #ceacde;
|
52
|
-
}
|
53
|
-
|
54
|
-
/* cyan */
|
55
|
-
.c36 {
|
56
|
-
color: #00cdd9;
|
57
|
-
}
|
58
|
-
|
59
|
-
/* black (but we can't use black, so a diff color) */
|
60
|
-
.c30 {
|
61
|
-
color: #666;
|
62
|
-
}
|
63
|
-
|
64
|
-
/* grey */
|
65
|
-
.c90 {
|
66
|
-
color: #838887;
|
67
|
-
}
|
68
|
-
|
69
|
-
body {
|
70
|
-
margin: 0;
|
71
|
-
padding: 0;
|
72
|
-
}
|
73
|
-
|
74
|
-
.previews {
|
75
|
-
overflow: hidden;
|
76
|
-
padding: 20px;
|
77
|
-
}
|
78
|
-
|
79
|
-
.preview-left, .preview-right {
|
80
|
-
width: 50%;
|
81
|
-
float: left;
|
82
|
-
}
|
83
|
-
|
84
|
-
.preview-left .code {
|
85
|
-
margin-right: 10px;
|
86
|
-
}
|
87
|
-
|
88
|
-
.preview-right .code {
|
89
|
-
margin-left: 10px;
|
90
|
-
}
|
7
|
+
.code {
|
8
|
+
background: #171717;
|
9
|
+
border-radius: 5px;
|
10
|
+
color: white;
|
11
|
+
word-break: break-word;
|
12
|
+
overflow-wrap: break-word;
|
13
|
+
font-family: Monaco, courier;
|
14
|
+
font-size: 12px;
|
15
|
+
line-height: 20px;
|
16
|
+
padding: 14px 18px;
|
17
|
+
white-space: pre-wrap;
|
18
|
+
margin-bottom: 15px;
|
19
|
+
}
|
20
|
+
|
21
|
+
body {
|
22
|
+
margin: 0;
|
23
|
+
padding: 20px;
|
24
|
+
}
|
25
|
+
|
26
|
+
.controls {
|
27
|
+
margin-bottom: 5px;
|
28
|
+
}
|
29
|
+
|
30
|
+
a {
|
31
|
+
font-family: Arial, sans-serif;
|
32
|
+
font-size: 13px;
|
33
|
+
}
|
91
34
|
</style>
|
35
|
+
<script>
|
36
|
+
function toggle(element) {
|
37
|
+
element.style.display = element.style.display === 'none' ? '' : 'none'
|
38
|
+
}
|
39
|
+
|
40
|
+
function switchViews() {
|
41
|
+
toggle(document.getElementById('rendered'));
|
42
|
+
toggle(document.getElementById('raw'));
|
43
|
+
}
|
44
|
+
</script>
|
92
45
|
</head>
|
93
46
|
<body>
|
94
|
-
<div class="
|
95
|
-
<div class="
|
96
|
-
<div class="
|
47
|
+
<div id="rendered" class="preview">
|
48
|
+
<div class="controls"><a href="#" onclick="switchViews(); return false">Show Raw Output</a></div>
|
49
|
+
<div class="code"><%= rendered %></div>
|
50
|
+
</div>
|
51
|
+
<div id="raw" class="preview" style="display: none">
|
52
|
+
<div class="controls"><a href="#" onclick="switchViews(); return false">Show Rendered Output</a></div>
|
53
|
+
<div class="code raw"><%= raw %></div>
|
97
54
|
</div>
|
98
55
|
</body>
|
99
56
|
</html>
|
data/lib/terminal/version.rb
CHANGED
data/script/benchmark
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$:.unshift(File.join(File.expand_path(File.dirname(__FILE__)), '..', 'lib'))
|
4
|
+
|
5
|
+
require 'terminal'
|
6
|
+
require 'benchmark/ips'
|
7
|
+
require 'timeout'
|
8
|
+
|
9
|
+
Benchmark.ips do |bm|
|
10
|
+
Dir.glob("spec/fixtures/*.raw").each do |file|
|
11
|
+
output = File.read(file)
|
12
|
+
basename = File.basename(file)
|
13
|
+
|
14
|
+
bm.report basename do
|
15
|
+
Timeout.timeout(100) do
|
16
|
+
Terminal.render(output)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|