terminal_rb 0.20.0 → 1.0.3
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 -484
- 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 +619 -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
data/lib/terminal/text.rb
CHANGED
|
@@ -1,132 +1,213 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'ansi'
|
|
4
|
+
require_relative 'text/formatter'
|
|
4
5
|
|
|
5
6
|
module Terminal
|
|
6
|
-
#
|
|
7
|
+
# Unicode-aware text processing utilities for display width calculation,
|
|
8
|
+
# word-wrapping, and formatted text output.
|
|
9
|
+
#
|
|
10
|
+
# All methods correctly handle multi-byte Unicode characters, East Asian
|
|
11
|
+
# wide characters, emoji, combining marks, and ANSI escape codes.
|
|
12
|
+
#
|
|
13
|
+
# @see Terminal::Text::Formatter
|
|
14
|
+
#
|
|
15
|
+
# @example Measure display width
|
|
16
|
+
# Terminal::Text.width('Hello') # => 5
|
|
17
|
+
# Terminal::Text.width("\u{1F600}") # => 2 (emoji)
|
|
18
|
+
#
|
|
19
|
+
# @example Word-wrap text
|
|
20
|
+
# Terminal::Text.lines('Hello World, this is a test', width: 12)
|
|
21
|
+
# # => ["Hello World,", "this is a", "test"]
|
|
7
22
|
#
|
|
8
23
|
module Text
|
|
9
24
|
class << self
|
|
10
|
-
#
|
|
11
|
-
# defined by the Unicode standard.
|
|
12
|
-
# Defaults to one (1).
|
|
25
|
+
# Display width used for East Asian ambiguous-width characters.
|
|
13
26
|
#
|
|
14
|
-
# @
|
|
27
|
+
# @example
|
|
28
|
+
# Terminal::Text.ambiguous_char_width # => 1
|
|
29
|
+
# Terminal::Text.ambiguous_char_width = 2 # for CJK terminals
|
|
15
30
|
#
|
|
16
|
-
# @return [Integer]
|
|
31
|
+
# @return [Integer] default: +1+
|
|
17
32
|
attr_accessor :ambiguous_char_width
|
|
18
33
|
|
|
19
|
-
#
|
|
20
|
-
# argument.
|
|
21
|
-
# It can optionally ignore embedded BBCode.
|
|
34
|
+
# Calculate the display width of a string in terminal columns.
|
|
22
35
|
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
36
|
+
# @example
|
|
37
|
+
# Terminal::Text.width('Hello') # => 5
|
|
38
|
+
# Terminal::Text.width('[bold]Hi[/bold]') # => 2
|
|
39
|
+
# Terminal::Text.width('[bold]Hi[/bold]', bbcode: false) # => 14
|
|
26
40
|
#
|
|
27
|
-
# @param str [#to_s]
|
|
28
|
-
# @param bbcode
|
|
29
|
-
# @
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
# @param str [#to_s] the string to measure
|
|
42
|
+
# @param bbcode (see Formatter#initialize)
|
|
43
|
+
# @param spaces (see Formatter#initialize)
|
|
44
|
+
# @return [Integer] display width in columns
|
|
45
|
+
def width(str, bbcode: true, spaces: true)
|
|
46
|
+
Formatter
|
|
47
|
+
.new(str, bbcode:, spaces:, ansi: false, eol: false)
|
|
48
|
+
.lines_with_size
|
|
49
|
+
.dig(0, -1) || 0
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Split text into lines with their display widths.
|
|
53
|
+
#
|
|
54
|
+
# @example
|
|
55
|
+
# Terminal::Text.lines_with_size('Hello World', width: 5)
|
|
56
|
+
# # => [["Hello", 5], ["World", 5]]
|
|
57
|
+
#
|
|
58
|
+
# @param (see Formatter#initialize)
|
|
59
|
+
# @param (see Formatter#lines_with_size)
|
|
60
|
+
# @return (see Formatter#lines_with_size)
|
|
61
|
+
def lines_with_size(
|
|
62
|
+
*str,
|
|
63
|
+
bbcode: true,
|
|
64
|
+
ansi: true,
|
|
65
|
+
spaces: true,
|
|
66
|
+
eol: true,
|
|
67
|
+
width: nil
|
|
68
|
+
)
|
|
69
|
+
Formatter.new(*str, bbcode:, ansi:, spaces:, eol:).lines_with_size(
|
|
70
|
+
width:
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Split text into lines as strings.
|
|
75
|
+
#
|
|
76
|
+
# @see Formatter#lines
|
|
77
|
+
#
|
|
78
|
+
# @example
|
|
79
|
+
# Terminal::Text.lines('Hello World', width: 5)
|
|
80
|
+
# # => ["Hello", "World"]
|
|
81
|
+
# @example With BBCode
|
|
82
|
+
# Terminal::Text.lines('[bold]Hello[/] World', width: 5)
|
|
83
|
+
# # => ["\e[1mHello\e[m", "World"]
|
|
84
|
+
#
|
|
85
|
+
# @param (see Formatter#initialize)
|
|
86
|
+
# @param (see Formatter#lines)
|
|
87
|
+
# @return (see Formatter#lines)
|
|
88
|
+
def lines(
|
|
89
|
+
*str,
|
|
90
|
+
bbcode: true,
|
|
91
|
+
ansi: true,
|
|
92
|
+
spaces: true,
|
|
93
|
+
eol: true,
|
|
94
|
+
width: nil
|
|
95
|
+
)
|
|
96
|
+
Formatter.new(*str, bbcode:, ansi:, spaces:, eol:).lines(width:)
|
|
40
97
|
end
|
|
41
98
|
|
|
42
|
-
#
|
|
99
|
+
# Format text with alignment, padding, and decorations.
|
|
43
100
|
#
|
|
44
|
-
# @
|
|
45
|
-
#
|
|
46
|
-
#
|
|
101
|
+
# @see Formatter.format
|
|
102
|
+
#
|
|
103
|
+
# @param (see Formatter.format)
|
|
104
|
+
# @return (see Formatter.format)
|
|
105
|
+
# @raise (see Formatter.format)
|
|
106
|
+
def format(...) = Formatter.format(...)
|
|
107
|
+
|
|
108
|
+
# @deprecated Use {.lines} and iterate over the result.
|
|
47
109
|
#
|
|
48
|
-
# @param
|
|
49
|
-
#
|
|
50
|
-
# @param
|
|
51
|
-
#
|
|
52
|
-
# @param
|
|
53
|
-
#
|
|
54
|
-
# @
|
|
55
|
-
# whether to keep embedded ANSI control codes
|
|
56
|
-
# @param ignore_newline [true, false]
|
|
57
|
-
# wheter to ignore embedded line breaks (`"\r\n"` or `"\n"`)
|
|
58
|
-
# @yield [String] text line
|
|
59
|
-
# @return [Enumerator] when no block given
|
|
60
|
-
# @return [nil]
|
|
61
|
-
# @raise ArgumentError when a `limit` less than `1` is given
|
|
110
|
+
# @comment @param str [Array<#to_s>] text to process
|
|
111
|
+
# @comment @param limit [Integer, nil] maximum line width
|
|
112
|
+
# @comment @param bbcode [true, false] process BBCode markup
|
|
113
|
+
# @comment @param ansi [true, false] recognize ANSI escape codes
|
|
114
|
+
# @comment @param ignore_newline [true, false] treat newlines as spaces
|
|
115
|
+
# @comment @yield [String] each line
|
|
116
|
+
# @comment @return [Enumerator, Object] enumerator or block result
|
|
62
117
|
def each_line(
|
|
63
|
-
*
|
|
118
|
+
*str,
|
|
64
119
|
limit: nil,
|
|
65
120
|
bbcode: true,
|
|
66
121
|
ansi: true,
|
|
67
122
|
ignore_newline: false,
|
|
68
|
-
&
|
|
123
|
+
&
|
|
69
124
|
)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
125
|
+
warn(
|
|
126
|
+
'[DEPRECATED] `each_line` is deprecated – use `.lines.each` instead',
|
|
127
|
+
category: :deprecated,
|
|
128
|
+
uplevel: 1
|
|
129
|
+
)
|
|
130
|
+
lines(
|
|
131
|
+
*str,
|
|
132
|
+
bbcode:,
|
|
133
|
+
ansi:,
|
|
134
|
+
spaces: true,
|
|
135
|
+
eol: !ignore_newline,
|
|
136
|
+
width: limit
|
|
137
|
+
).each(&)
|
|
79
138
|
end
|
|
80
139
|
alias each each_line
|
|
81
140
|
|
|
82
|
-
#
|
|
141
|
+
# @deprecated Use {.lines_with_size} and iterate over the result.
|
|
83
142
|
#
|
|
84
|
-
# @
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
#
|
|
88
|
-
# @param
|
|
89
|
-
# @yield [String, Integer]
|
|
90
|
-
# @return
|
|
91
|
-
# @raise (see each_line)
|
|
143
|
+
# @comment @param str [Array<to_s>] text to process
|
|
144
|
+
# @comment @param limit [Integer, nil] maximum line width
|
|
145
|
+
# @comment @param bbcode [true, false] process BBCode markup
|
|
146
|
+
# @comment @param ansi [true, false] recognize ANSI escape codes
|
|
147
|
+
# @comment @param ignore_newline [true, false] treat newlines as spaces
|
|
148
|
+
# @comment @yield [String, Integer] each line and its display width
|
|
149
|
+
# @comment @return [Enumerator, Object] enumerator or block result
|
|
92
150
|
def each_line_with_size(
|
|
93
|
-
*
|
|
151
|
+
*str,
|
|
94
152
|
limit: nil,
|
|
95
153
|
bbcode: true,
|
|
96
154
|
ansi: true,
|
|
97
155
|
ignore_newline: false,
|
|
98
|
-
&
|
|
156
|
+
&
|
|
99
157
|
)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
158
|
+
warn(
|
|
159
|
+
'[DEPRECATED] `each_line_with_size` is deprecated ' \
|
|
160
|
+
'– use `.lines_width_size.each` instead',
|
|
161
|
+
category: :deprecated,
|
|
162
|
+
uplevel: 1
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
lines_with_size(
|
|
166
|
+
*str,
|
|
167
|
+
bbcode:,
|
|
168
|
+
ansi:,
|
|
169
|
+
spaces: true,
|
|
170
|
+
eol: !ignore_newline,
|
|
171
|
+
width: limit
|
|
172
|
+
).each(&)
|
|
109
173
|
end
|
|
110
174
|
alias each_with_size each_line_with_size
|
|
111
175
|
|
|
112
|
-
#
|
|
176
|
+
# Calculate the maximum display width of any line.
|
|
113
177
|
#
|
|
114
|
-
# @
|
|
115
|
-
#
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
178
|
+
# @example
|
|
179
|
+
# Terminal::Text.max_line_width("short\na longer line")
|
|
180
|
+
# # => 13
|
|
181
|
+
#
|
|
182
|
+
# @param (see Formatter#initialize)
|
|
183
|
+
# @return (see Formatter#max_line_width)
|
|
184
|
+
def max_line_width(
|
|
185
|
+
*str,
|
|
186
|
+
ansi: true,
|
|
187
|
+
bbcode: true,
|
|
188
|
+
spaces: true,
|
|
189
|
+
eol: true,
|
|
190
|
+
**opts
|
|
191
|
+
)
|
|
192
|
+
# backward compatibility:
|
|
193
|
+
if opts.key?(:ignore_newline)
|
|
194
|
+
eol = opts[:ignore_newline] ? false : true
|
|
195
|
+
warn(
|
|
196
|
+
"Parameter ':ignore_newline' will become obsolete - " \
|
|
197
|
+
"use 'eol: #{eol.inspect}' instead",
|
|
198
|
+
category: :deprecated
|
|
199
|
+
)
|
|
200
|
+
end
|
|
120
201
|
|
|
121
|
-
|
|
202
|
+
Formatter.new(*str, bbcode:, ansi:, spaces:, eol:).max_line_width
|
|
203
|
+
end
|
|
122
204
|
|
|
205
|
+
# @private
|
|
123
206
|
def char_width(char)
|
|
124
207
|
ord = char.ord
|
|
125
208
|
return @ctrlchar_width[ord] if ord < 0x20
|
|
126
209
|
if char.size == 1
|
|
127
|
-
return 1
|
|
128
|
-
width = CharWidth[ord]
|
|
129
|
-
return width < 0 ? @ambiguous_char_width : width
|
|
210
|
+
return ord < 0xa1 ? 1 : CharWidth[ord] || @ambiguous_char_width
|
|
130
211
|
end
|
|
131
212
|
sum = 0
|
|
132
213
|
zwj = false
|
|
@@ -134,369 +215,13 @@ module Terminal
|
|
|
134
215
|
next zwj = false if zwj
|
|
135
216
|
ord = c.ord
|
|
136
217
|
next zwj = true if ord == 0x200d # zero with joiner
|
|
137
|
-
|
|
138
|
-
sum += (width < 0 ? @ambiguous_char_width : width)
|
|
218
|
+
sum += CharWidth[ord] || @ambiguous_char_width
|
|
139
219
|
end
|
|
140
220
|
sum
|
|
141
221
|
end
|
|
142
|
-
|
|
143
|
-
def lim_pairs(snippets, limit)
|
|
144
|
-
line = @empty.dup
|
|
145
|
-
size = 0
|
|
146
|
-
csi = nil
|
|
147
|
-
snippets.each do |snippet|
|
|
148
|
-
if snippet == :space
|
|
149
|
-
next if size == 0
|
|
150
|
-
next line << ' ' if (size += 1) <= limit
|
|
151
|
-
yield(line, size - 1)
|
|
152
|
-
line = "#{csi}"
|
|
153
|
-
next size = 0
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
if snippet == :nl
|
|
157
|
-
line[-1] == ' ' ? yield(line.chop, size - 1) : yield(line, size)
|
|
158
|
-
line = "#{csi}"
|
|
159
|
-
next size = 0
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
if snippet == :hard_nl
|
|
163
|
-
line[-1] == ' ' ? yield(line.chop, size - 1) : yield(line, size)
|
|
164
|
-
line = @empty.dup
|
|
165
|
-
csi = nil
|
|
166
|
-
next size = 0
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
if snippet == CsiEnd
|
|
170
|
-
line << CsiEnd if csi
|
|
171
|
-
next csi = nil
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
next line << (csi = snippet) if Csi === snippet
|
|
175
|
-
next line << snippet if Osc === snippet
|
|
176
|
-
|
|
177
|
-
# Word:
|
|
178
|
-
|
|
179
|
-
if (ns = size + snippet.size) <= limit
|
|
180
|
-
line << snippet
|
|
181
|
-
next size = ns
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
if line[-1] == ' '
|
|
185
|
-
line.chop!
|
|
186
|
-
size -= 1
|
|
187
|
-
end
|
|
188
|
-
yield(line, size) if size != 0
|
|
189
|
-
|
|
190
|
-
if snippet.size <= limit
|
|
191
|
-
line = "#{csi}#{snippet}"
|
|
192
|
-
next size = snippet.size
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
words = snippet.split(limit)
|
|
196
|
-
if words[-1].size <= limit
|
|
197
|
-
snippet = words.pop
|
|
198
|
-
line = "#{csi}#{snippet}"
|
|
199
|
-
size = snippet.size
|
|
200
|
-
else
|
|
201
|
-
line = "#{csi}"
|
|
202
|
-
size = 0
|
|
203
|
-
end
|
|
204
|
-
words.each { yield("#{csi}#{_1}", _1.size) }
|
|
205
|
-
end
|
|
206
|
-
nil
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
def max_width_of(snippets)
|
|
210
|
-
lws = false
|
|
211
|
-
ret = size = 0
|
|
212
|
-
snippets.each do |snippet|
|
|
213
|
-
if snippet == :space
|
|
214
|
-
next if size == 0
|
|
215
|
-
lws = true
|
|
216
|
-
next size += 1
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
if snippet == :nl || snippet == :hard_nl
|
|
220
|
-
size -= 1 if lws
|
|
221
|
-
ret = size if ret < size
|
|
222
|
-
lws = false
|
|
223
|
-
next size = 0
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
lws = false
|
|
227
|
-
size += snippet.size if Word === snippet
|
|
228
|
-
end
|
|
229
|
-
ret
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
def pairs(snippets)
|
|
233
|
-
line = @empty.dup
|
|
234
|
-
size = 0
|
|
235
|
-
csi = nil
|
|
236
|
-
snippets.each do |snippet|
|
|
237
|
-
if snippet == :space
|
|
238
|
-
next if size == 0
|
|
239
|
-
line << ' '
|
|
240
|
-
next size += 1
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
if snippet == :nl
|
|
244
|
-
line[-1] == ' ' ? yield(line.chop, size - 1) : yield(line, size)
|
|
245
|
-
line = "#{csi}"
|
|
246
|
-
next size = 0
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
if snippet == :hard_nl
|
|
250
|
-
line[-1] == ' ' ? yield(line.chop, size - 1) : yield(line, size)
|
|
251
|
-
line = @empty.dup
|
|
252
|
-
csi = nil
|
|
253
|
-
next size = 0
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
if snippet == CsiEnd
|
|
257
|
-
line << CsiEnd if csi
|
|
258
|
-
next csi = nil
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
next line << (csi = snippet) if Csi === snippet
|
|
262
|
-
next line << snippet if Osc === snippet
|
|
263
|
-
|
|
264
|
-
# Word:
|
|
265
|
-
size += snippet.size
|
|
266
|
-
line << snippet
|
|
267
|
-
end
|
|
268
|
-
nil
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
def lim_lines(snippets, limit)
|
|
272
|
-
line = @empty.dup
|
|
273
|
-
size = 0
|
|
274
|
-
csi = nil
|
|
275
|
-
snippets.each do |snippet|
|
|
276
|
-
if snippet == :space
|
|
277
|
-
next if size == 0
|
|
278
|
-
next line << ' ' if (size += 1) <= limit
|
|
279
|
-
yield(line)
|
|
280
|
-
line = "#{csi}"
|
|
281
|
-
next size = 0
|
|
282
|
-
end
|
|
283
|
-
|
|
284
|
-
if snippet == :nl
|
|
285
|
-
yield(line[-1] == ' ' ? line.chop : line)
|
|
286
|
-
line = "#{csi}"
|
|
287
|
-
next size = 0
|
|
288
|
-
end
|
|
289
|
-
|
|
290
|
-
if snippet == :hard_nl
|
|
291
|
-
yield(line[-1] == ' ' ? line.chop : line)
|
|
292
|
-
line = @empty.dup
|
|
293
|
-
csi = nil
|
|
294
|
-
next size = 0
|
|
295
|
-
end
|
|
296
|
-
|
|
297
|
-
if snippet == CsiEnd
|
|
298
|
-
line << CsiEnd if csi
|
|
299
|
-
next csi = nil
|
|
300
|
-
end
|
|
301
|
-
|
|
302
|
-
next line << (csi = snippet) if Csi === snippet
|
|
303
|
-
next line << snippet if Osc === snippet
|
|
304
|
-
|
|
305
|
-
# Word:
|
|
306
|
-
|
|
307
|
-
if (ns = size + snippet.size) <= limit
|
|
308
|
-
line << snippet
|
|
309
|
-
next size = ns
|
|
310
|
-
end
|
|
311
|
-
|
|
312
|
-
if line[-1] == ' '
|
|
313
|
-
line.chop!
|
|
314
|
-
size -= 1
|
|
315
|
-
end
|
|
316
|
-
yield(line) if size != 0
|
|
317
|
-
|
|
318
|
-
if snippet.size <= limit
|
|
319
|
-
line = "#{csi}#{snippet}"
|
|
320
|
-
next size = snippet.size
|
|
321
|
-
end
|
|
322
|
-
|
|
323
|
-
words = snippet.split(limit)
|
|
324
|
-
if words[-1].size <= limit
|
|
325
|
-
snippet = words.pop
|
|
326
|
-
line = "#{csi}#{snippet}"
|
|
327
|
-
size = snippet.size
|
|
328
|
-
else
|
|
329
|
-
line = "#{csi}"
|
|
330
|
-
size = 0
|
|
331
|
-
end
|
|
332
|
-
words.each { yield("#{csi}#{_1}") }
|
|
333
|
-
end
|
|
334
|
-
nil
|
|
335
|
-
end
|
|
336
|
-
|
|
337
|
-
def lines(snippets)
|
|
338
|
-
line = @empty.dup
|
|
339
|
-
size = 0
|
|
340
|
-
csi = nil
|
|
341
|
-
snippets.each do |snippet|
|
|
342
|
-
if snippet == :space
|
|
343
|
-
next if size == 0
|
|
344
|
-
line << ' '
|
|
345
|
-
next size += 1
|
|
346
|
-
end
|
|
347
|
-
|
|
348
|
-
if snippet == :nl
|
|
349
|
-
yield(line[-1] == ' ' ? line.chop : line)
|
|
350
|
-
line = "#{csi}"
|
|
351
|
-
next size = 0
|
|
352
|
-
end
|
|
353
|
-
|
|
354
|
-
if snippet == :hard_nl
|
|
355
|
-
yield(line[-1] == ' ' ? line.chop : line)
|
|
356
|
-
line = @empty.dup
|
|
357
|
-
csi = nil
|
|
358
|
-
next size = 0
|
|
359
|
-
end
|
|
360
|
-
|
|
361
|
-
if snippet == CsiEnd
|
|
362
|
-
line << CsiEnd if csi
|
|
363
|
-
next csi = nil
|
|
364
|
-
end
|
|
365
|
-
|
|
366
|
-
next line << (csi = snippet) if Csi === snippet
|
|
367
|
-
next line << snippet if Osc === snippet
|
|
368
|
-
|
|
369
|
-
# Word:
|
|
370
|
-
size += snippet.size
|
|
371
|
-
line << snippet
|
|
372
|
-
end
|
|
373
|
-
nil
|
|
374
|
-
end
|
|
375
|
-
|
|
376
|
-
def as_snippets(text, bbcode, ansi, ignore_newline, word_class)
|
|
377
|
-
ret = []
|
|
378
|
-
last = nil
|
|
379
|
-
to_s = bbcode ? ->(s) { Ansi.bbcode(s) } : :to_s.to_proc
|
|
380
|
-
text.each do |txt|
|
|
381
|
-
if (txt = to_s[txt]).empty?
|
|
382
|
-
next ret[-1] = last = :hard_nl if Symbol === last
|
|
383
|
-
next ret << (last = :hard_nl)
|
|
384
|
-
end
|
|
385
|
-
|
|
386
|
-
(txt.encoding == @encoding ? txt : txt.encode(@encoding)).scan(
|
|
387
|
-
@scan_snippet
|
|
388
|
-
) do |nl, csi, osc, space, gc|
|
|
389
|
-
if gc
|
|
390
|
-
next last.add(gc, char_width(gc)) if word_class === last
|
|
391
|
-
next ret << (last = word_class.new(gc, char_width(gc)))
|
|
392
|
-
end
|
|
393
|
-
|
|
394
|
-
next Symbol === last ? nil : ret << (last = :space) if space
|
|
395
|
-
|
|
396
|
-
if nl
|
|
397
|
-
if ignore_newline # handle nl like space
|
|
398
|
-
next Symbol === last ? nil : ret << (last = :space)
|
|
399
|
-
end
|
|
400
|
-
next last == :space ? ret[-1] = last = :nl : ret << (last = :nl)
|
|
401
|
-
end
|
|
402
|
-
|
|
403
|
-
next unless ansi
|
|
404
|
-
|
|
405
|
-
next ret << (last = Osc.new(osc)) if osc
|
|
406
|
-
|
|
407
|
-
if csi == "\e[m" || csi == "\e[0m"
|
|
408
|
-
next last == CsiEnd ? nil : ret << (last = CsiEnd)
|
|
409
|
-
end
|
|
410
|
-
|
|
411
|
-
Csi === last ? last.add(csi) : ret << (last = Csi.new(csi))
|
|
412
|
-
end
|
|
413
|
-
|
|
414
|
-
Symbol === last ? ret[-1] = last = :hard_nl : ret << (last = :hard_nl)
|
|
415
|
-
end
|
|
416
|
-
ret
|
|
417
|
-
end
|
|
418
|
-
end
|
|
419
|
-
|
|
420
|
-
class Osc
|
|
421
|
-
attr_reader :to_str, :size
|
|
422
|
-
alias to_s to_str
|
|
423
|
-
|
|
424
|
-
def initialize(str)
|
|
425
|
-
@to_str = str
|
|
426
|
-
@size = 0
|
|
427
|
-
end
|
|
428
|
-
end
|
|
429
|
-
|
|
430
|
-
class Csi < Osc
|
|
431
|
-
def add(str) = (@to_str << str)
|
|
432
|
-
end
|
|
433
|
-
|
|
434
|
-
module CsiEnd
|
|
435
|
-
class << self
|
|
436
|
-
attr_reader :to_str, :size
|
|
437
|
-
end
|
|
438
|
-
@to_str = "\e[m"
|
|
439
|
-
@size = 0
|
|
440
|
-
end
|
|
441
|
-
|
|
442
|
-
class Word
|
|
443
|
-
attr_reader :to_str, :size
|
|
444
|
-
alias to_s to_str
|
|
445
|
-
|
|
446
|
-
def initialize(char, size)
|
|
447
|
-
@to_str = char.dup
|
|
448
|
-
@size = size
|
|
449
|
-
end
|
|
450
|
-
|
|
451
|
-
def add(char, size)
|
|
452
|
-
@to_str << char
|
|
453
|
-
@size += size
|
|
454
|
-
end
|
|
455
|
-
end
|
|
456
|
-
|
|
457
|
-
class WordEx < Word
|
|
458
|
-
attr_reader :chars
|
|
459
|
-
|
|
460
|
-
def initialize(char, size)
|
|
461
|
-
super
|
|
462
|
-
@chars = [[char, size]]
|
|
463
|
-
end
|
|
464
|
-
|
|
465
|
-
def add(char, size)
|
|
466
|
-
@chars << [char, size]
|
|
467
|
-
super
|
|
468
|
-
end
|
|
469
|
-
|
|
470
|
-
def split(limit)
|
|
471
|
-
chars = @chars.dup
|
|
472
|
-
ret = [last = Word.new(*chars.shift)]
|
|
473
|
-
chars.each do |c, s|
|
|
474
|
-
next ret << (last = Word.new(c, s)) if last.size + s > limit
|
|
475
|
-
last.add(c, s)
|
|
476
|
-
end
|
|
477
|
-
ret
|
|
478
|
-
end
|
|
479
222
|
end
|
|
480
223
|
|
|
481
224
|
@ambiguous_char_width = 1
|
|
482
|
-
@empty = String.new(encoding: @encoding = Encoding::UTF_8).freeze
|
|
483
|
-
|
|
484
|
-
@scan_snippet =
|
|
485
|
-
/\G(?:
|
|
486
|
-
(\r?\n)
|
|
487
|
-
| (\e\[[\x30-\x3f]*[\x20-\x2f]*[a-zA-Z])
|
|
488
|
-
| (\e\]\d+(?:;[^\a\e]+)*(?:\a|\e\\))
|
|
489
|
-
| (\s+)
|
|
490
|
-
| (\X)
|
|
491
|
-
)/x
|
|
492
|
-
|
|
493
|
-
@scan_width =
|
|
494
|
-
/\G(?:
|
|
495
|
-
(?:\e\[[\x30-\x3f]*[\x20-\x2f]*[a-zA-Z])
|
|
496
|
-
| (?:\e\]\d+(?:;[^\a\e]+)*(?:\a|\e\\))
|
|
497
|
-
| (\s+)
|
|
498
|
-
| (\X)
|
|
499
|
-
)/x
|
|
500
225
|
|
|
501
226
|
@ctrlchar_width =
|
|
502
227
|
Hash
|
|
@@ -541,7 +266,6 @@ module Terminal
|
|
|
541
266
|
cw_file = "#{__dir__}/text/char_width.rb"
|
|
542
267
|
autoload :CharWidth, cw_file
|
|
543
268
|
autoload :UNICODE_VERSION, cw_file
|
|
544
|
-
|
|
545
|
-
private_constant :Osc, :Csi, :CsiEnd, :Word, :WordEx, :CharWidth
|
|
269
|
+
private_constant :CharWidth
|
|
546
270
|
end
|
|
547
271
|
end
|
data/lib/terminal/version.rb
CHANGED