terminal-layout 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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