terminal_rb 0.20.0 → 1.0.4
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/README.md +5 -4
- data/bin/bbcode +25 -21
- data/examples/24bit-colors.rb +3 -3
- data/examples/3bit-colors.rb +9 -9
- data/examples/8bit-colors.rb +18 -18
- data/examples/attributes.rb +24 -10
- data/examples/bbcode.rb +23 -19
- data/examples/info.rb +7 -7
- data/examples/key-codes.rb +4 -4
- data/examples/screen_viewer.rb +82 -0
- data/examples/text.rb +12 -28
- data/lib/terminal/ansi/named_colors.rb +1 -0
- data/lib/terminal/ansi/screen_viewer.rb +224 -0
- data/lib/terminal/ansi.rb +502 -480
- data/lib/terminal/detect.rb +1 -0
- data/lib/terminal/input/ansi.rb +9 -7
- data/lib/terminal/input/dumb.rb +5 -8
- data/lib/terminal/input/key_event.rb +131 -75
- data/lib/terminal/input.rb +49 -40
- data/lib/terminal/output/ansi.rb +39 -5
- data/lib/terminal/output/dumb.rb +33 -0
- data/lib/terminal/output.rb +139 -125
- data/lib/terminal/rspec/helper.rb +30 -1
- data/lib/terminal/shell.rb +10 -6
- data/lib/terminal/text/char_width.rb +178 -176
- data/lib/terminal/text/formatter.rb +614 -0
- data/lib/terminal/text.rb +168 -444
- data/lib/terminal/version.rb +1 -1
- data/lib/terminal.rb +79 -75
- metadata +9 -7
- data/terminal_rb.gemspec +0 -36
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Terminal
|
|
4
|
+
module Text
|
|
5
|
+
# Processes text with BBCode markup and ANSI escape codes into
|
|
6
|
+
# display-width-aware lines. Supports Unicode-aware word-wrapping,
|
|
7
|
+
# alignment, padding, and prefix/suffix decorations.
|
|
8
|
+
#
|
|
9
|
+
# @see Terminal::Text
|
|
10
|
+
#
|
|
11
|
+
# @example Basic word-wrapping
|
|
12
|
+
# fmt = Terminal::Text::Formatter.new('Hello World, this is a test')
|
|
13
|
+
# fmt.lines(width: 12)
|
|
14
|
+
# # => ["Hello World,", "this is a", "test"]
|
|
15
|
+
#
|
|
16
|
+
# @example Formatted output with alignment
|
|
17
|
+
# Terminal::Text::Formatter.format(
|
|
18
|
+
# 'Hello', align: :center, width: 20
|
|
19
|
+
# )
|
|
20
|
+
# # => [" Hello "]
|
|
21
|
+
class Formatter
|
|
22
|
+
# Parse text into lines, optionally with display widths.
|
|
23
|
+
#
|
|
24
|
+
# @see #lines
|
|
25
|
+
# @see #lines_with_size
|
|
26
|
+
#
|
|
27
|
+
# @example
|
|
28
|
+
# Terminal::Text::Formatter['Hello World', width: 5]
|
|
29
|
+
# # => ["Hello", "World"]
|
|
30
|
+
# @example With size information
|
|
31
|
+
# Terminal::Text::Formatter['Hello', with_size: true]
|
|
32
|
+
# # => [["Hello", 5]]
|
|
33
|
+
#
|
|
34
|
+
# @param (see #initialize)
|
|
35
|
+
# @param with_size [true, false] when +true+,
|
|
36
|
+
# return +[line, width]+ pairs instead of plain strings
|
|
37
|
+
# @param (see #lines_with_size)
|
|
38
|
+
# @return (see #lines_with_size)
|
|
39
|
+
def self.[](
|
|
40
|
+
*str,
|
|
41
|
+
with_size: false,
|
|
42
|
+
ansi: true,
|
|
43
|
+
bbcode: true,
|
|
44
|
+
spaces: true,
|
|
45
|
+
eol: true,
|
|
46
|
+
width: nil
|
|
47
|
+
)
|
|
48
|
+
ret = new(*str, ansi:, bbcode:, spaces:, eol:).lines_with_size(width:)
|
|
49
|
+
with_size ? ret : ret.map(&:first)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Format text with alignment, padding, and decorations.
|
|
53
|
+
#
|
|
54
|
+
# @see #format
|
|
55
|
+
#
|
|
56
|
+
# @param (see #initialize)
|
|
57
|
+
# @param (see #format)
|
|
58
|
+
# @result (see #format)
|
|
59
|
+
# @raise (see #format)
|
|
60
|
+
def self.format(
|
|
61
|
+
*str,
|
|
62
|
+
ansi: true,
|
|
63
|
+
bbcode: true,
|
|
64
|
+
spaces: true,
|
|
65
|
+
eol: true,
|
|
66
|
+
align: nil,
|
|
67
|
+
width: nil,
|
|
68
|
+
height: nil,
|
|
69
|
+
padding: nil,
|
|
70
|
+
prefix: nil,
|
|
71
|
+
suffix: nil
|
|
72
|
+
)
|
|
73
|
+
new(*str, ansi:, bbcode:, spaces:, eol:).format(
|
|
74
|
+
align:,
|
|
75
|
+
width:,
|
|
76
|
+
height:,
|
|
77
|
+
padding:,
|
|
78
|
+
prefix:,
|
|
79
|
+
suffix:
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Create a new formatter with given text and options.
|
|
84
|
+
#
|
|
85
|
+
# @param str [Array<#to_s>] text to process
|
|
86
|
+
# @param ansi [true, false] recognize ANSI escape codes
|
|
87
|
+
# @param bbcode [true, false] process BBCode markup
|
|
88
|
+
# @param spaces [true, false] preserve whitespace;
|
|
89
|
+
# when +false+ leading/trailing spaces and multiple spaces are
|
|
90
|
+
# collapsed
|
|
91
|
+
# @param eol [true, false] preserve line endings;
|
|
92
|
+
# when +false+ newlines are treated as spaces
|
|
93
|
+
def initialize(*str, ansi: true, bbcode: true, spaces: true, eol: true)
|
|
94
|
+
@lex = []
|
|
95
|
+
return if str.empty?
|
|
96
|
+
ansi ? _generate_ansi(str, bbcode) : _generate(str, bbcode)
|
|
97
|
+
return if @lex.empty?
|
|
98
|
+
@lex = _ignore_whitespace unless spaces
|
|
99
|
+
@lex = _ignore_newline unless eol
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Word-wrap and return lines with their display widths.
|
|
103
|
+
#
|
|
104
|
+
# @example
|
|
105
|
+
# fmt = Terminal::Text::Formatter.new('Hello World')
|
|
106
|
+
# fmt.lines_with_size(width: 5)
|
|
107
|
+
# # => [["Hello", 5], ["World", 5]]
|
|
108
|
+
#
|
|
109
|
+
# @param width [Integer, nil] maximum line width in columns;
|
|
110
|
+
# +nil+ returns unwrapped lines
|
|
111
|
+
# @return [Array<String, Integer>] pairs of
|
|
112
|
+
# +[line_text, display_width]+
|
|
113
|
+
# @raise (see #format)
|
|
114
|
+
def lines_with_size(width: nil)
|
|
115
|
+
return [] if @lex.empty?
|
|
116
|
+
return @unlimited || _unlimited unless width
|
|
117
|
+
(w = width.to_i) > 0 and return _limited(w)
|
|
118
|
+
raise(ArgumentError, "invalid width - #{width.inspect}")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Word-wrap and return lines as strings.
|
|
122
|
+
#
|
|
123
|
+
# @example
|
|
124
|
+
# fmt = Terminal::Text::Formatter.new('Hello World')
|
|
125
|
+
# fmt.lines(width: 5)
|
|
126
|
+
# # => ["Hello", "World"]
|
|
127
|
+
#
|
|
128
|
+
# @param width [Integer, nil] maximum line width in columns
|
|
129
|
+
# @return [Array<String>]
|
|
130
|
+
# @raise [ArgumentError] if +width+ is zero or negative
|
|
131
|
+
def lines(width: nil) = lines_with_size(width:).map(&:first)
|
|
132
|
+
|
|
133
|
+
# Format lines with alignment, padding, and decorations.
|
|
134
|
+
#
|
|
135
|
+
# @example Centered text with padding
|
|
136
|
+
# fmt = Terminal::Text::Formatter.new('Hi')
|
|
137
|
+
# fmt.format(align: :center, width: 10, padding: [0, 1])
|
|
138
|
+
# # => [" Hi "]
|
|
139
|
+
#
|
|
140
|
+
# @param align [Symbol, nil] text alignment:
|
|
141
|
+
# +:left+, +:right+, +:center+, or +nil+ (no fill)
|
|
142
|
+
# @param width [Integer, nil] output line width in columns
|
|
143
|
+
# @param height [Integer, nil] number of output lines;
|
|
144
|
+
# negative values take lines from the end
|
|
145
|
+
# @param padding [Integer, Array, nil] CSS-style padding
|
|
146
|
+
# - 1 value: all sides;
|
|
147
|
+
# - 2 values: [vertical, horizontal];
|
|
148
|
+
# - 3 values: [top, horizontal, bottom];
|
|
149
|
+
# - 4 values: [top, right, bottom, left]
|
|
150
|
+
# @param prefix [#to_s, nil] string prepended to each line
|
|
151
|
+
# @param suffix [#to_s, nil] string appended to each line
|
|
152
|
+
# @return [Array<String>] formatted output lines
|
|
153
|
+
# @raise [ArgumentError] if +width+ is zero or negative
|
|
154
|
+
def format(
|
|
155
|
+
align: nil,
|
|
156
|
+
width: nil,
|
|
157
|
+
height: nil,
|
|
158
|
+
padding: nil,
|
|
159
|
+
prefix: nil,
|
|
160
|
+
suffix: nil
|
|
161
|
+
)
|
|
162
|
+
top, right, bottom, left = _padding(padding)
|
|
163
|
+
|
|
164
|
+
if height
|
|
165
|
+
return [] if (height = height.to_i) == 0
|
|
166
|
+
if height < 0
|
|
167
|
+
tail = true
|
|
168
|
+
height = -height
|
|
169
|
+
end
|
|
170
|
+
height -= top
|
|
171
|
+
height -= -bottom
|
|
172
|
+
return [] if height < 1
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
if width
|
|
176
|
+
w = (width = width.to_i) - left - right
|
|
177
|
+
raise(ArgumentError, "invalid width - #{width.inspect}") if w < 1
|
|
178
|
+
lws = _limited(w)
|
|
179
|
+
lws = tail ? lws.last(height) : lws.take(height) if height
|
|
180
|
+
empty = "#{prefix}#{' ' * width}#{suffix}" if top + bottom > 0
|
|
181
|
+
else
|
|
182
|
+
lws = @unlimited || _unlimited
|
|
183
|
+
lws = tail ? lws.last(height) : lws.take(height) if height
|
|
184
|
+
w = lws.empty? ? 0 : lws.max_by(&:last)[-1]
|
|
185
|
+
if top + bottom > 0
|
|
186
|
+
empty = "#{prefix}#{' ' * (w + left + right)}#{suffix}"
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
ret = []
|
|
191
|
+
ret.fill(empty, 0, top) if top > 0
|
|
192
|
+
left = left < 1 ? prefix.to_s : "#{prefix}#{' ' * left}"
|
|
193
|
+
right = right < 1 ? suffix.to_s : "#{' ' * right}#{suffix}"
|
|
194
|
+
|
|
195
|
+
space = SPACE_CACHE.dup
|
|
196
|
+
|
|
197
|
+
case align
|
|
198
|
+
when :left
|
|
199
|
+
lws.each { |l, s| ret << "#{left}#{l}#{space[w - s]}#{right}" }
|
|
200
|
+
when :right
|
|
201
|
+
lws.each { |l, s| ret << "#{left}#{space[w - s]}#{l}#{right}" }
|
|
202
|
+
when :center
|
|
203
|
+
lws.each do |l, s|
|
|
204
|
+
ls = (s = w - s) / 2
|
|
205
|
+
next ret << "#{left}#{l}#{right}" if ls < 1
|
|
206
|
+
ret << "#{left}#{space[ls]}#{l}#{space[s - ls]}#{right}"
|
|
207
|
+
end
|
|
208
|
+
else
|
|
209
|
+
lws.each { |l, _| ret << "#{left}#{l}#{right}" }
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
return ret.fill(empty, ret.size, bottom) if bottom > 0
|
|
213
|
+
ret.empty? ? ret << '' : ret
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Whether the formatter has any content.
|
|
217
|
+
#
|
|
218
|
+
# @return [true, false]
|
|
219
|
+
def empty? = @lex.empty?
|
|
220
|
+
|
|
221
|
+
# Maximum display width of any line.
|
|
222
|
+
#
|
|
223
|
+
# @example
|
|
224
|
+
# fmt = Terminal::Text::Formatter.new("short\na longer line")
|
|
225
|
+
# fmt.max_line_width # => 13
|
|
226
|
+
#
|
|
227
|
+
# @return [Integer] widest line in columns, +0+ when empty
|
|
228
|
+
def max_line_width
|
|
229
|
+
@max_line_width ||=
|
|
230
|
+
(@lex.empty? ? 0 : (@unlimited || _unlimited).max_by(&:last)[-1])
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# @private
|
|
234
|
+
# Convert to a single string with lines joined by newlines.
|
|
235
|
+
#
|
|
236
|
+
# @example
|
|
237
|
+
# Terminal::Text::Formatter.new('Hello World').to_s(width: 5)
|
|
238
|
+
# # => "Hello\nWorld"
|
|
239
|
+
#
|
|
240
|
+
# @param width [Integer, nil] maximum line width in columns
|
|
241
|
+
# @return [String]
|
|
242
|
+
def to_s(width: nil) = lines(width:).join("\n")
|
|
243
|
+
|
|
244
|
+
private
|
|
245
|
+
|
|
246
|
+
def _padding(value)
|
|
247
|
+
return Array.new(4, [0, value.to_i].max) unless value.respond_to?(:to_a)
|
|
248
|
+
value = value.to_a.take(4).map! { [0, it.to_i].max }
|
|
249
|
+
case value.size
|
|
250
|
+
when 0
|
|
251
|
+
value.fill(0, 0, 4)
|
|
252
|
+
when 1
|
|
253
|
+
value.fill(value[0], 0, 4)
|
|
254
|
+
when 2
|
|
255
|
+
value * 2
|
|
256
|
+
when 3
|
|
257
|
+
value << value[1]
|
|
258
|
+
else
|
|
259
|
+
value
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def _limited(width)
|
|
264
|
+
ret = []
|
|
265
|
+
line = []
|
|
266
|
+
csi = []
|
|
267
|
+
size = 0
|
|
268
|
+
|
|
269
|
+
add_line = -> do
|
|
270
|
+
if size == 0 # Osc, Csi, CsiEnd only
|
|
271
|
+
ret << [+'', 0]
|
|
272
|
+
next line = [], size = 0
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
if csi.empty?
|
|
276
|
+
ret << [line.join, size]
|
|
277
|
+
next line = [], size = 0
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
if csi.size == 1 && line[-1].is_a?(Csi) # Csi at end
|
|
281
|
+
line.pop
|
|
282
|
+
ret << [line.join, size]
|
|
283
|
+
next line = [], size = 0
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
line << CsiEnd if line[-1] != CsiEnd
|
|
287
|
+
ret << [line.join, size]
|
|
288
|
+
line = csi.dup
|
|
289
|
+
size = 0
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
@lex.each do |current|
|
|
293
|
+
if current.is_a?(NewLine)
|
|
294
|
+
if (last = line[-1]).is_a?(Space) # ignore trailing space
|
|
295
|
+
line.pop
|
|
296
|
+
size -= last.size
|
|
297
|
+
end
|
|
298
|
+
add_line.call # if size > 0
|
|
299
|
+
next
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# ignore leading space:
|
|
303
|
+
next if line.empty? && current.is_a?(Space) && current.size == 1
|
|
304
|
+
|
|
305
|
+
ns = size + current.size
|
|
306
|
+
|
|
307
|
+
# fits in width:
|
|
308
|
+
if ns < width
|
|
309
|
+
if current == CsiEnd
|
|
310
|
+
next if csi.empty? # useless
|
|
311
|
+
|
|
312
|
+
if csi.size == 1 && line[-1].is_a?(Csi) # Csi at end
|
|
313
|
+
line.pop
|
|
314
|
+
else
|
|
315
|
+
line << CsiEnd
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
next csi = []
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
csi << current if current.is_a?(Csi)
|
|
322
|
+
next line << current, size = ns
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# fill-up the line:
|
|
326
|
+
if ns == width
|
|
327
|
+
# ignore trailing space:
|
|
328
|
+
next add_line.call if current.is_a?(Space)
|
|
329
|
+
line << current
|
|
330
|
+
size = width
|
|
331
|
+
next add_line.call
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# exceeding the width:
|
|
335
|
+
|
|
336
|
+
# force flush:
|
|
337
|
+
if (last = line[-1]).is_a?(Space) # ignore trailing space
|
|
338
|
+
line.pop
|
|
339
|
+
size -= last.size
|
|
340
|
+
end
|
|
341
|
+
add_line.call if size > 0
|
|
342
|
+
|
|
343
|
+
next if current.is_a?(Space) # ignore trailing space
|
|
344
|
+
|
|
345
|
+
# handle Word...
|
|
346
|
+
splitted = current.split(width)
|
|
347
|
+
last = splitted.pop
|
|
348
|
+
|
|
349
|
+
if csi.empty?
|
|
350
|
+
splitted.each { ret << [it.to_str, it.size] }
|
|
351
|
+
else
|
|
352
|
+
csi_str = csi.join
|
|
353
|
+
splitted.each { ret << ["#{csi_str}#{it.to_str}\e[m", it.size] }
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
line = csi.dup
|
|
357
|
+
next size = 0 unless last
|
|
358
|
+
line << last
|
|
359
|
+
size = last.size
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
ret.pop if ret[-1] == ['', 0]
|
|
363
|
+
ret
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def _unlimited
|
|
367
|
+
@unlimited = []
|
|
368
|
+
line = []
|
|
369
|
+
@lex.each do |current|
|
|
370
|
+
next line << current unless current.is_a?(NewLine)
|
|
371
|
+
@unlimited << [line.join.freeze, line.sum(&:size)]
|
|
372
|
+
line = []
|
|
373
|
+
end
|
|
374
|
+
@unlimited << [line.join.freeze, line.sum(&:size)] unless line.empty?
|
|
375
|
+
@unlimited.freeze
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def _generate(text, bbcode)
|
|
379
|
+
bbcode = bbcode ? ->(s) { Ansi.unbbcode(s) } : :to_s.to_proc
|
|
380
|
+
|
|
381
|
+
text.each do |line|
|
|
382
|
+
next @lex << EOP if (line = bbcode[line]).empty?
|
|
383
|
+
|
|
384
|
+
line.encode!(ENCODING) if line.encoding != ENCODING
|
|
385
|
+
|
|
386
|
+
line.scan(SCAN_REGEX) do |newline, space, _, _, char|
|
|
387
|
+
if char
|
|
388
|
+
(last = @lex[-1]).is_a?(Word) and next last.add(char)
|
|
389
|
+
next @lex << Word.new(char)
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
if space
|
|
393
|
+
(last = @lex[-1]).is_a?(Space) and next last.inc
|
|
394
|
+
next @lex << Space.new
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
next @lex << EOL if newline
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
@lex[-1] == EOL ? @lex[-1] = EOP : @lex << EOP
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
nil
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def _generate_ansi(text, bbcode)
|
|
407
|
+
bbcode = bbcode ? ->(s) { Ansi.bbcode(s) } : :to_s.to_proc
|
|
408
|
+
|
|
409
|
+
text.each do |line|
|
|
410
|
+
next @lex << EOP if (line = bbcode[line]).empty?
|
|
411
|
+
|
|
412
|
+
line.encode!(ENCODING) if line.encoding != ENCODING
|
|
413
|
+
csis = 0
|
|
414
|
+
|
|
415
|
+
line.scan(SCAN_REGEX) do |newline, space, csi, osc, char|
|
|
416
|
+
if char
|
|
417
|
+
(last = @lex[-1]).is_a?(Word) and next last.add(char)
|
|
418
|
+
next @lex << Word.new(char)
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
if space
|
|
422
|
+
(last = @lex[-1]).is_a?(Space) and next last.inc
|
|
423
|
+
next @lex << Space.new
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
next @lex << EOL if newline
|
|
427
|
+
|
|
428
|
+
next @lex << Osc.new(osc) if osc
|
|
429
|
+
|
|
430
|
+
# Handle csi...
|
|
431
|
+
if csi == "\e[m" || csi == "\e[0m"
|
|
432
|
+
next if csis == 0
|
|
433
|
+
|
|
434
|
+
if @lex[-1].is_a?(Csi)
|
|
435
|
+
@lex.pop
|
|
436
|
+
next csis = 0 if csis == 1
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
csis = 0
|
|
440
|
+
next @lex << CsiEnd
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
(last = @lex[-1]).is_a?(Csi) and next last.add(csi)
|
|
444
|
+
|
|
445
|
+
csis += 1
|
|
446
|
+
@lex << Csi.new(csi)
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
@lex.pop if (last = @lex[-1]) == EOL
|
|
450
|
+
|
|
451
|
+
next @lex << EOP if csis == 0
|
|
452
|
+
|
|
453
|
+
if last.is_a?(Csi) && csis == 1
|
|
454
|
+
@lex.pop
|
|
455
|
+
else
|
|
456
|
+
@lex << CsiEnd
|
|
457
|
+
end
|
|
458
|
+
@lex << EOP
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
nil
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def _ignore_whitespace
|
|
465
|
+
ret = []
|
|
466
|
+
last = nil
|
|
467
|
+
space = Space.new.freeze
|
|
468
|
+
|
|
469
|
+
@lex.each do |current|
|
|
470
|
+
if current.is_a?(Space)
|
|
471
|
+
next if last.nil? || last.is_a?(NewLine) # skip leading space
|
|
472
|
+
next ret << (last = space)
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
if current.is_a?(NewLine) # remove trailing space
|
|
476
|
+
if last == space
|
|
477
|
+
ret.pop
|
|
478
|
+
elsif last == CsiEnd && ret[-2] == space
|
|
479
|
+
ret.delete_at(-2)
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
ret << (last = current)
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
ret
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def _ignore_newline
|
|
490
|
+
last = nil
|
|
491
|
+
|
|
492
|
+
@lex.filter_map do |current|
|
|
493
|
+
if current == EOL
|
|
494
|
+
next if last.nil? || last == EOP || last.is_a?(Space)
|
|
495
|
+
next last = Space.new
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
if current.is_a?(Space) && last.is_a?(Space)
|
|
499
|
+
last.size = last.size == 1 ? current.size : last.size + current.size
|
|
500
|
+
next
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
last = current
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
# @private
|
|
508
|
+
class Space
|
|
509
|
+
attr_accessor :size
|
|
510
|
+
def to_str = (' ' * @size)
|
|
511
|
+
def initialize = (@size = 1)
|
|
512
|
+
def inc = (@size += 1)
|
|
513
|
+
def inspect = @size == 1 ? '<Space>' : "<Space #{@size}>"
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
# @private
|
|
517
|
+
class Word
|
|
518
|
+
def to_str = (@to_str ||= @chars.join)
|
|
519
|
+
def size = (@size ||= @sizes.sum)
|
|
520
|
+
def inspect = "<Word #{@sizes.sum}:#{@chars.join.inspect}>"
|
|
521
|
+
|
|
522
|
+
def initialize(char, size = Text.char_width(char))
|
|
523
|
+
@chars = [char]
|
|
524
|
+
@sizes = [size]
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def add(char, size = Text.char_width(char))
|
|
528
|
+
@chars << char
|
|
529
|
+
@sizes << size
|
|
530
|
+
nil
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def split(width)
|
|
534
|
+
return [self] if @chars.size == 1
|
|
535
|
+
ret = last = size = nil
|
|
536
|
+
@chars.each_with_index do |char, idx|
|
|
537
|
+
csize = @sizes[idx]
|
|
538
|
+
next ret = [last = Word.new(char, size = csize)] unless ret
|
|
539
|
+
next last.add(char, csize) if (size += csize) <= width
|
|
540
|
+
ret << (last = Word.new(char, size = csize))
|
|
541
|
+
end
|
|
542
|
+
ret
|
|
543
|
+
end
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
# @private
|
|
547
|
+
class Osc
|
|
548
|
+
attr_reader :to_str
|
|
549
|
+
def size = 0
|
|
550
|
+
def initialize(str) = (@to_str = str)
|
|
551
|
+
def inspect = "<Osc #{@to_str.inspect}>"
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
# @private
|
|
555
|
+
class Csi
|
|
556
|
+
attr_reader :to_str
|
|
557
|
+
def size = 0
|
|
558
|
+
def initialize(str) = (@to_str = str)
|
|
559
|
+
def add(str) = (@to_str << str unless @to_str.include?(str))
|
|
560
|
+
def inspect = "<Csi #{@to_str.inspect}>"
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
# @private
|
|
564
|
+
module CsiEnd
|
|
565
|
+
class << self
|
|
566
|
+
attr_reader :to_str, :size, :inspect
|
|
567
|
+
end
|
|
568
|
+
@to_str = "\e[m"
|
|
569
|
+
@size = 0
|
|
570
|
+
@inspect = '<CsiEnd>'
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
# @private
|
|
574
|
+
class NewLine
|
|
575
|
+
attr_reader :inspect
|
|
576
|
+
def initialize(inspect) = (@inspect = inspect)
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
EOL = NewLine.new '<EOL>'
|
|
580
|
+
EOP = NewLine.new '<EOP>'
|
|
581
|
+
|
|
582
|
+
ENCODING = Encoding::UTF_8
|
|
583
|
+
|
|
584
|
+
SPACE_CACHE =
|
|
585
|
+
Hash
|
|
586
|
+
.new { |h, s| h[s] = s < 1 ? '' : ' ' * s }
|
|
587
|
+
.compare_by_identity
|
|
588
|
+
.freeze
|
|
589
|
+
|
|
590
|
+
SCAN_REGEX =
|
|
591
|
+
/\G(?:
|
|
592
|
+
(\r?\n)
|
|
593
|
+
| (\s)
|
|
594
|
+
| (\e\[[\x30-\x3f]*[\x20-\x2f]*[a-zA-Z])
|
|
595
|
+
| (\e\]\d+(?:;[^\a\e]+)*(?:\a|\e\\))
|
|
596
|
+
| (\X)
|
|
597
|
+
)/x
|
|
598
|
+
|
|
599
|
+
private_constant(
|
|
600
|
+
:Space,
|
|
601
|
+
:Word,
|
|
602
|
+
:Osc,
|
|
603
|
+
:Csi,
|
|
604
|
+
:CsiEnd,
|
|
605
|
+
:NewLine,
|
|
606
|
+
:EOL,
|
|
607
|
+
:EOP,
|
|
608
|
+
:ENCODING,
|
|
609
|
+
:SPACE_CACHE,
|
|
610
|
+
:SCAN_REGEX
|
|
611
|
+
)
|
|
612
|
+
end
|
|
613
|
+
end
|
|
614
|
+
end
|