terminal-layout 0.1.0
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 +7 -0
- data/.rspec +2 -0
- data/.travis.yml +14 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +60 -0
- data/README.md +7 -0
- data/block-flow.rb +361 -0
- data/lib/ansi_string.rb +315 -0
- data/lib/terminal_layout.rb +527 -0
- data/spec/ansi_string_spec.rb +499 -0
- data/spec/spec_helper.rb +102 -0
- data/spec/terminal_layout_spec.rb +745 -0
- data/terminal-layout.gemspec +28 -0
- data/test-1.rb +90 -0
- metadata +158 -0
data/lib/ansi_string.rb
ADDED
@@ -0,0 +1,315 @@
|
|
1
|
+
class ANSIString
|
2
|
+
attr_reader :raw, :without_ansi
|
3
|
+
|
4
|
+
def initialize(str)
|
5
|
+
process_string raw_string_for(str)
|
6
|
+
end
|
7
|
+
|
8
|
+
def +(other)
|
9
|
+
self.class.new @raw + raw_string_for(other)
|
10
|
+
end
|
11
|
+
|
12
|
+
def <<(other)
|
13
|
+
range = length..length
|
14
|
+
str = replace_in_string(range, other)
|
15
|
+
process_string raw_string_for(str)
|
16
|
+
self
|
17
|
+
end
|
18
|
+
|
19
|
+
def [](range)
|
20
|
+
# convert numeric position to a range
|
21
|
+
range = (range..range) if range.is_a?(Integer)
|
22
|
+
|
23
|
+
range_begin = range.begin
|
24
|
+
range_end = range.exclude_end? ? range.end - 1 : range.end
|
25
|
+
|
26
|
+
range_begin = @without_ansi.length - range.begin.abs if range.begin < 0
|
27
|
+
range_end = @without_ansi.length - range.end.abs if range.end < 0
|
28
|
+
|
29
|
+
str = build_string_with_ansi_for(range_begin..range_end)
|
30
|
+
if str
|
31
|
+
ANSIString.new str
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def []=(range, replacement_str)
|
36
|
+
# convert numeric position to a range
|
37
|
+
range = (range..range) if range.is_a?(Integer)
|
38
|
+
|
39
|
+
text = @without_ansi[range]
|
40
|
+
process_string replace_in_string(range, replacement_str)
|
41
|
+
self
|
42
|
+
end
|
43
|
+
|
44
|
+
# See String#index for arguments
|
45
|
+
def index(*args)
|
46
|
+
@without_ansi.index(*args)
|
47
|
+
end
|
48
|
+
|
49
|
+
def match(*args, &blk)
|
50
|
+
@without_ansi.match(*args, &blk)
|
51
|
+
end
|
52
|
+
|
53
|
+
# See String#rindex for arguments
|
54
|
+
def rindex(*args)
|
55
|
+
@without_ansi.rindex(*args)
|
56
|
+
end
|
57
|
+
|
58
|
+
def reverse
|
59
|
+
str = @ansi_sequence_locations.reverse.map do |location|
|
60
|
+
[location[:start_ansi_sequence], location[:text].reverse, location[:end_ansi_sequence]].join
|
61
|
+
end.join
|
62
|
+
ANSIString.new str
|
63
|
+
end
|
64
|
+
|
65
|
+
def slice(index, length=nil)
|
66
|
+
range = nil
|
67
|
+
index = index.without_ansi if index.is_a?(ANSIString)
|
68
|
+
index = Regexp.new Regexp.escape(index) if index.is_a?(String)
|
69
|
+
if index.is_a?(Integer)
|
70
|
+
length = index unless length
|
71
|
+
range = (index...index+length)
|
72
|
+
elsif index.is_a?(Range)
|
73
|
+
range = index
|
74
|
+
elsif index.is_a?(Regexp)
|
75
|
+
md = @without_ansi.match(index)
|
76
|
+
capture_group_index = length || 0
|
77
|
+
if md
|
78
|
+
capture_group = md.offset(capture_group_index)
|
79
|
+
range = (capture_group.first...capture_group.last)
|
80
|
+
end
|
81
|
+
else
|
82
|
+
raise(ArgumentError, "Must pass in at least an index or a range.")
|
83
|
+
end
|
84
|
+
self[range] if range
|
85
|
+
end
|
86
|
+
|
87
|
+
def split(*args)
|
88
|
+
raw.split(*args).map { |s| ANSIString.new(s) }
|
89
|
+
end
|
90
|
+
|
91
|
+
def length
|
92
|
+
@without_ansi.length
|
93
|
+
end
|
94
|
+
|
95
|
+
def lines
|
96
|
+
result = []
|
97
|
+
current_string = ""
|
98
|
+
@ansi_sequence_locations.map do |location|
|
99
|
+
if location[:text] == "\n"
|
100
|
+
result << ANSIString.new(current_string + "\n")
|
101
|
+
current_string = ""
|
102
|
+
next
|
103
|
+
end
|
104
|
+
|
105
|
+
location[:text].scan(/.*(?:\n|$)/).each_with_index do |line, i|
|
106
|
+
break if line == ""
|
107
|
+
|
108
|
+
if i == 0
|
109
|
+
current_string << [
|
110
|
+
location[:start_ansi_sequence],
|
111
|
+
line,
|
112
|
+
location[:end_ansi_sequence]
|
113
|
+
].join
|
114
|
+
else
|
115
|
+
result << ANSIString.new(current_string)
|
116
|
+
current_string = ""
|
117
|
+
current_string << [
|
118
|
+
location[:start_ansi_sequence],
|
119
|
+
line,
|
120
|
+
location[:end_ansi_sequence]
|
121
|
+
].join
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
if location[:text].end_with?("\n")
|
126
|
+
result << ANSIString.new(current_string)
|
127
|
+
current_string = ""
|
128
|
+
next
|
129
|
+
end
|
130
|
+
end
|
131
|
+
result << ANSIString.new(current_string) if current_string.length > 0
|
132
|
+
result
|
133
|
+
end
|
134
|
+
|
135
|
+
def dup
|
136
|
+
ANSIString.new(@raw.dup)
|
137
|
+
end
|
138
|
+
|
139
|
+
def sub(pattern, replacement)
|
140
|
+
str = ""
|
141
|
+
count = 0
|
142
|
+
max_count = 1
|
143
|
+
index = 0
|
144
|
+
@without_ansi.enum_for(:scan, pattern).each do
|
145
|
+
md = Regexp.last_match
|
146
|
+
str << build_string_with_ansi_for(index...(index + md.begin(0)))
|
147
|
+
index = md.end(0)
|
148
|
+
break if (count += 1) == max_count
|
149
|
+
end
|
150
|
+
if index != @without_ansi.length
|
151
|
+
str << build_string_with_ansi_for(index..@without_ansi.length)
|
152
|
+
end
|
153
|
+
nstr = str.gsub /(\033\[[0-9;]*m)(.+?)\033\[0m\1/, '\1\2'
|
154
|
+
ANSIString.new(nstr)
|
155
|
+
end
|
156
|
+
|
157
|
+
def to_s
|
158
|
+
@raw.dup
|
159
|
+
end
|
160
|
+
alias :to_str :to_s
|
161
|
+
|
162
|
+
def ==(other)
|
163
|
+
(other.class == self.class && other.raw == @raw) || (other.kind_of?(String) && other == @raw)
|
164
|
+
end
|
165
|
+
|
166
|
+
private
|
167
|
+
|
168
|
+
def raw_string_for(str)
|
169
|
+
str.is_a?(ANSIString) ? str.raw : str.to_s
|
170
|
+
end
|
171
|
+
|
172
|
+
def process_string(raw_str)
|
173
|
+
@without_ansi = ""
|
174
|
+
@ansi_sequence_locations = []
|
175
|
+
raw_str.enum_for(:scan, /(\e\[[0-9;]*m)?(.*?)(?=\e\[[0-9;]*m|\Z)/m ).each do
|
176
|
+
md = Regexp.last_match
|
177
|
+
ansi_sequence, text = md.captures
|
178
|
+
|
179
|
+
previous_sequence_location = @ansi_sequence_locations.last
|
180
|
+
if previous_sequence_location
|
181
|
+
if ansi_sequence == "\e[0m"
|
182
|
+
previous_sequence_location[:end_ansi_sequence] = ansi_sequence
|
183
|
+
ansi_sequence = nil
|
184
|
+
elsif previous_sequence_location[:start_ansi_sequence] == ansi_sequence
|
185
|
+
previous_sequence_location[:text] << text
|
186
|
+
previous_sequence_location[:ends_at] += text.length
|
187
|
+
previous_sequence_location[:length] += text.length
|
188
|
+
@without_ansi << text
|
189
|
+
next
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
if ansi_sequence.nil? && text.to_s.length == 0
|
194
|
+
next
|
195
|
+
end
|
196
|
+
|
197
|
+
@ansi_sequence_locations.push(
|
198
|
+
begins_at: @without_ansi.length,
|
199
|
+
ends_at: [@without_ansi.length + text.length - 1, 0].max,
|
200
|
+
length: text.length,
|
201
|
+
text: text,
|
202
|
+
start_ansi_sequence: ansi_sequence
|
203
|
+
)
|
204
|
+
|
205
|
+
@without_ansi << text
|
206
|
+
end
|
207
|
+
|
208
|
+
@raw = @ansi_sequence_locations.map do |location|
|
209
|
+
[location[:start_ansi_sequence], location[:text], location[:end_ansi_sequence]].compact.join
|
210
|
+
end.join
|
211
|
+
|
212
|
+
@ansi_sequence_locations
|
213
|
+
end
|
214
|
+
|
215
|
+
def replace_in_string(range, replacement_str)
|
216
|
+
raise RangeError, "#{range.inspect} out of range" if range.begin > length
|
217
|
+
return replacement_str if @ansi_sequence_locations.empty?
|
218
|
+
|
219
|
+
range = range.begin..(range.end - 1) if range.exclude_end?
|
220
|
+
str = ""
|
221
|
+
|
222
|
+
@ansi_sequence_locations.each_with_index do |location, j|
|
223
|
+
# If the given range encompasses part of the location, then we want to
|
224
|
+
# include the whole location
|
225
|
+
if location[:begins_at] >= range.begin && location[:ends_at] <= range.end
|
226
|
+
end_index = range.end - location[:begins_at] + 1
|
227
|
+
|
228
|
+
str << [
|
229
|
+
location[:start_ansi_sequence],
|
230
|
+
replacement_str,
|
231
|
+
location[:text][end_index..-1],
|
232
|
+
location[:end_ansi_sequence]
|
233
|
+
].join
|
234
|
+
|
235
|
+
# If the location falls within the given range then make sure we pull
|
236
|
+
# out the bits that we want, and keep ANSI escape sequenece intact while
|
237
|
+
# doing so.
|
238
|
+
elsif location[:begins_at] <= range.begin && location[:ends_at] >= range.end
|
239
|
+
start_index = range.begin - location[:begins_at]
|
240
|
+
end_index = range.end - location[:begins_at] + 1
|
241
|
+
|
242
|
+
str << [
|
243
|
+
location[:start_ansi_sequence],
|
244
|
+
location[:text][0...start_index],
|
245
|
+
replacement_str,
|
246
|
+
location[:text][end_index..-1],
|
247
|
+
location[:end_ansi_sequence]
|
248
|
+
].join
|
249
|
+
|
250
|
+
elsif location[:ends_at] == range.begin
|
251
|
+
start_index = range.begin - location[:begins_at]
|
252
|
+
end_index = range.end
|
253
|
+
num_chars_to_remove_from_next_location = range.end - location[:ends_at]
|
254
|
+
|
255
|
+
str << [
|
256
|
+
location[:start_ansi_sequence],
|
257
|
+
location[:text][location[:begins_at]...(location[:begins_at]+start_index)],
|
258
|
+
location[:end_ansi_sequence],
|
259
|
+
replacement_str.to_s,
|
260
|
+
location[:text][end_index..-1],
|
261
|
+
].join
|
262
|
+
|
263
|
+
if location=@ansi_sequence_locations[j+1]
|
264
|
+
old = location.dup
|
265
|
+
location[:text][0...num_chars_to_remove_from_next_location] = ""
|
266
|
+
location[:begins_at] += num_chars_to_remove_from_next_location
|
267
|
+
location[:ends_at] += num_chars_to_remove_from_next_location
|
268
|
+
end
|
269
|
+
|
270
|
+
# If we're pushing onto the end of the string
|
271
|
+
elsif range.begin == length && location[:ends_at] == length - 1
|
272
|
+
if replacement_str.is_a?(ANSIString)
|
273
|
+
str << [location[:start_ansi_sequence], location[:text], location[:end_ansi_sequence], replacement_str].join
|
274
|
+
else
|
275
|
+
str << [location[:start_ansi_sequence], location[:text], replacement_str, location[:end_ansi_sequence]].join
|
276
|
+
end
|
277
|
+
else
|
278
|
+
str << [location[:start_ansi_sequence], location[:text], location[:end_ansi_sequence]].join
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
str
|
283
|
+
end
|
284
|
+
|
285
|
+
def build_string_with_ansi_for(range)
|
286
|
+
return nil if range.begin > length
|
287
|
+
|
288
|
+
str = ""
|
289
|
+
|
290
|
+
if range.exclude_end?
|
291
|
+
range = range.begin..(range.end - 1)
|
292
|
+
end
|
293
|
+
|
294
|
+
@ansi_sequence_locations.each do |location|
|
295
|
+
# If the given range encompasses part of the location, then we want to
|
296
|
+
# include the whole location
|
297
|
+
if location[:begins_at] >= range.begin && location[:ends_at] <= range.end
|
298
|
+
str << [location[:start_ansi_sequence], location[:text], location[:end_ansi_sequence]].join
|
299
|
+
|
300
|
+
elsif location[:begins_at] >= range.begin && location[:begins_at] <= range.end
|
301
|
+
str << [location[:start_ansi_sequence], location[:text][range.begin..(range.end - location[:begins_at])], location[:end_ansi_sequence]].join
|
302
|
+
|
303
|
+
# If the location falls within the given range then make sure we pull
|
304
|
+
# out the bits that we want, and keep ANSI escape sequenece intact while
|
305
|
+
# doing so.
|
306
|
+
elsif (location[:begins_at] <= range.begin && location[:ends_at] >= range.end) || range.cover?(location[:ends_at])
|
307
|
+
start_index = range.begin - location[:begins_at]
|
308
|
+
end_index = range.end - location[:begins_at]
|
309
|
+
str << [location[:start_ansi_sequence], location[:text][start_index..end_index], location[:end_ansi_sequence]].join
|
310
|
+
end
|
311
|
+
end
|
312
|
+
str
|
313
|
+
end
|
314
|
+
|
315
|
+
end
|