terminal_rb 0.9.1 → 0.9.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.
data/lib/terminal/text.rb CHANGED
@@ -41,6 +41,10 @@ module Terminal
41
41
 
42
42
  # Iterate each line of given text.
43
43
  #
44
+ # @example Generate word-by-word wrapped text for a limited output width
45
+ # Terminal::Text.each_line('This is a simple test 😀', limit: 6).to_a
46
+ # # => ["This", "is a", "simple", "test", "😀"]
47
+ #
44
48
  # @param [#to_s, ...] text
45
49
  # text objects to process
46
50
  # @param [#to_i, nil] limit
@@ -66,12 +70,12 @@ module Terminal
66
70
  if limit
67
71
  limit = limit.to_i
68
72
  raise(ArgumentError, "invalid limit - #{limit}") if limit < 1
69
- if block_given?
70
- lines_in(text, bbcode, ansi, ignore_newline, limit, &block)
73
+ if block
74
+ lim_lines(text, bbcode, ansi, ignore_newline, limit, &block)
71
75
  else
72
- to_enum(:lines_in, text, bbcode, ansi, ignore_newline, limit)
76
+ to_enum(:lim_lines, text, bbcode, ansi, ignore_newline, limit)
73
77
  end
74
- elsif block_given?
78
+ elsif block
75
79
  lines(text, bbcode, ansi, ignore_newline, &block)
76
80
  else
77
81
  to_enum(:lines, text, bbcode, ansi, ignore_newline)
@@ -81,6 +85,10 @@ module Terminal
81
85
 
82
86
  # Iterate each line and it's display width of given text.
83
87
  #
88
+ # @example Generate word-by-word wrapped text for a limited output width
89
+ # Terminal::Text.each_line_with_size('This is a simple test 😀', limit: 6).to_a
90
+ # # => [["This", 4], ["is a", 4], ["simple", 6], ["test", 4], ["😀", 2]]
91
+ #
84
92
  # @param (see each_line)
85
93
  # @yield [String, Integer] text line and it's display width
86
94
  # @return (see each_line)
@@ -96,12 +104,12 @@ module Terminal
96
104
  if limit
97
105
  limit = limit.to_i
98
106
  raise(ArgumentError, "invalid limit - #{limit}") if limit < 1
99
- if block_given?
100
- pairs_in(text, bbcode, ansi, ignore_newline, limit, &block)
107
+ if block
108
+ lim_pairs(text, bbcode, ansi, ignore_newline, limit, &block)
101
109
  else
102
- to_enum(:pairs_in, text, bbcode, ansi, ignore_newline, limit)
110
+ to_enum(:lim_pairs, text, bbcode, ansi, ignore_newline, limit)
103
111
  end
104
- elsif block_given?
112
+ elsif block
105
113
  pairs(text, bbcode, ansi, ignore_newline, &block)
106
114
  else
107
115
  to_enum(:pairs, text, bbcode, ansi, ignore_newline)
@@ -122,315 +130,322 @@ module Terminal
122
130
  sco == 0xff9e || sco == 0xff9f ? 2 : 1
123
131
  end
124
132
 
125
- def each_part(text, bbcode, ansi, ignore_newline)
126
- newline = ignore_newline ? :space : :nl
127
- text.each do |txt|
128
- txt = bbcode ? Ansi.bbcode(txt) : txt.to_s
129
- next yield(:hard_nl) if txt.empty?
130
- txt = txt.encode(ENC) if txt.encoding != ENC
131
- word = nil
132
- txt.scan(SCAN_EXPR) do |nl, csi, osc, space, gc|
133
- if gc
134
- next word.add(gc, char_width(gc)) if word
135
- next word = Word.new(gc, char_width(gc))
136
- end
137
- yield(word) if word
138
- word = nil
139
- next yield(:space) if space
140
- next yield(newline) if nl
141
- next unless ansi
142
- next yield(:seq, osc) if osc
143
- next yield(:seq_end) if csi == "\e[m" || csi == "\e[0m"
144
- yield(:seq, csi)
133
+ def lim_pairs(text, bbcode, ansi, ignore_newline, limit)
134
+ line = EMPTY.dup
135
+ size = 0
136
+ csi = nil
137
+ as_snippeds(
138
+ text,
139
+ bbcode,
140
+ ansi,
141
+ ignore_newline,
142
+ WordEx
143
+ ).each do |snipped|
144
+ if snipped == :space
145
+ next if size == 0
146
+ next line << ' ' if (size += 1) <= limit
147
+ yield(line, size - 1)
148
+ line = "#{csi}"
149
+ next size = 0
145
150
  end
146
- yield(word) if word
147
- yield(:hard_nl)
148
- end
149
- nil
150
- end
151
151
 
152
- def lines(text, bbcode, ansi, ignore_newline)
153
- current = EMPTY.dup
154
- seq = EMPTY.dup
155
- lws = nil
156
- each_part(text, bbcode, ansi, ignore_newline) do |part, opt|
157
- if part == :space
158
- next if lws
159
- current << ' '
160
- next lws = true
152
+ if snipped == :nl
153
+ yield(line, size)
154
+ line = "#{csi}"
155
+ next size = 0
161
156
  end
162
157
 
163
- if part.is_a?(Word)
164
- current << part
165
- next lws = false
158
+ if snipped == :hard_nl
159
+ yield(line, size)
160
+ line = EMPTY.dup
161
+ csi = nil
162
+ next size = 0
166
163
  end
167
164
 
168
- if part == :nl
169
- yield(lws ? current.chop : current)
170
- current = seq.dup
171
- next lws = false
165
+ if snipped == CsiEnd
166
+ line << CsiEnd if csi
167
+ next csi = nil
172
168
  end
173
169
 
174
- if part == :hard_nl
175
- yield(lws ? current.chop : current)
176
- seq.clear
177
- current = EMPTY.dup
178
- next lws = false
170
+ next line << (csi = snipped) if snipped.is_a?(Csi)
171
+ next line << snipped if snipped.is_a?(Osc)
172
+
173
+ # Word:
174
+
175
+ if (ns = size + snipped.size) <= limit
176
+ line << snipped
177
+ next size = ns
179
178
  end
180
179
 
181
- lws = false
180
+ if line[-1] == ' '
181
+ line.chop!
182
+ size -= 1
183
+ end
184
+ yield(line, size) if size != 0
182
185
 
183
- if part == :seq
184
- current << opt
185
- next seq << opt
186
+ if snipped.size < limit
187
+ line = "#{csi}#{snipped}"
188
+ next size = snipped.size
186
189
  end
187
190
 
188
- # :seq_end
189
- current << "\e[m"
190
- seq.clear
191
+ words = snipped.split(limit)
192
+ if words[-1].size < limit
193
+ snipped = words.pop
194
+ line = "#{csi}#{snipped}"
195
+ size = snipped.size
196
+ else
197
+ line = "#{csi}"
198
+ size = 0
199
+ end
200
+ words.each { yield("#{csi}#{_1}", _1.size) }
191
201
  end
192
202
  nil
193
203
  end
194
204
 
195
- def lines_in(text, bbcode, ansi, ignore_newline, limit)
196
- current = EMPTY.dup
197
- seq = EMPTY.dup
198
- width = 0
199
- lws = nil
200
- each_part(text, bbcode, ansi, ignore_newline) do |part, opt|
201
- if part == :space
202
- next if lws
203
- if width.succ < limit
204
- current << ' '
205
- width += 1
206
- next lws = true
207
- end
208
- yield(current)
209
- current = seq.dup
210
- width = 0
211
- next lws = false
205
+ def lim_lines(text, bbcode, ansi, ignore_newline, limit)
206
+ line = EMPTY.dup
207
+ size = 0
208
+ csi = nil
209
+ as_snippeds(
210
+ text,
211
+ bbcode,
212
+ ansi,
213
+ ignore_newline,
214
+ WordEx
215
+ ).each do |snipped|
216
+ if snipped == :space
217
+ next if size == 0
218
+ next line << ' ' if (size += 1) <= limit
219
+ yield(line)
220
+ line = "#{csi}"
221
+ next size = 0
212
222
  end
213
223
 
214
- if part.is_a?(Word)
215
- if (nw = width + part.size) <= limit
216
- current << part
217
- width = nw
218
- next lws = false
219
- end
224
+ if snipped == :nl
225
+ yield(line)
226
+ line = "#{csi}"
227
+ next size = 0
228
+ end
220
229
 
221
- yield(lws ? current.chop : current) if width > 0
222
- current = seq.dup
230
+ if snipped == :hard_nl
231
+ yield(line)
232
+ line = EMPTY.dup
233
+ csi = nil
234
+ next size = 0
235
+ end
223
236
 
224
- if part.size < limit
225
- current << part
226
- width = part.size
227
- next lws = false
228
- end
237
+ if snipped == CsiEnd
238
+ line << CsiEnd if csi
239
+ next csi = nil
240
+ end
229
241
 
230
- if part.size == limit
231
- yield(current + part)
232
- current = seq.dup
233
- width = 0
234
- next lws = false
235
- end
242
+ next line << (csi = snipped) if snipped.is_a?(Csi)
243
+ next line << snipped if snipped.is_a?(Osc)
236
244
 
237
- width = 0
238
- part.chars.each do |c, w|
239
- next current << c if (width += w) <= limit
240
- yield(current)
241
- current = seq.dup << c
242
- width = w
243
- end
245
+ # Word:
244
246
 
245
- next lws = false
247
+ if (ns = size + snipped.size) <= limit
248
+ line << snipped
249
+ next size = ns
246
250
  end
247
251
 
248
- if part == :nl
249
- yield(lws ? current.chop : current)
250
- current = seq.dup
251
- width = 0
252
- next lws = false
252
+ if line[-1] == ' '
253
+ line.chop!
254
+ size -= 1
253
255
  end
256
+ yield(line) if size != 0
254
257
 
255
- if part == :hard_nl
256
- yield(lws ? current.chop : current) if width > 0
257
- seq.clear
258
- current = EMPTY.dup
259
- width = 0
260
- next lws = false
258
+ if snipped.size < limit
259
+ line = "#{csi}#{snipped}"
260
+ next size = snipped.size
261
261
  end
262
262
 
263
- lws = false
264
-
265
- if part == :seq
266
- current << opt
267
- next seq << opt
263
+ words = snipped.split(limit)
264
+ if words[-1].size < limit
265
+ snipped = words.pop
266
+ line = "#{csi}#{snipped}"
267
+ size = snipped.size
268
+ else
269
+ line = "#{csi}"
270
+ size = 0
268
271
  end
269
-
270
- # :seq_end
271
- next if seq.empty?
272
- current << "\e[m"
273
- seq.clear
272
+ words.each { yield("#{csi}#{_1}") }
274
273
  end
275
274
  nil
276
275
  end
277
276
 
278
277
  def pairs(text, bbcode, ansi, ignore_newline)
279
- current = EMPTY.dup
280
- seq = EMPTY.dup
281
- width = 0
282
- lws = nil
283
- each_part(text, bbcode, ansi, ignore_newline) do |part, opt|
284
- if part == :space
285
- next if lws
286
- current << ' '
287
- width += 1
288
- next lws = true
278
+ line = EMPTY.dup
279
+ size = 0
280
+ csi = nil
281
+ as_snippeds(text, bbcode, ansi, ignore_newline, Word).each do |snipped|
282
+ if snipped == :space
283
+ next if size == 0
284
+ line << ' '
285
+ next size += 1
289
286
  end
290
287
 
291
- if part.is_a?(Word)
292
- current << part
293
- width += part.size
294
- next lws = false
288
+ if snipped == :nl
289
+ yield(line, size)
290
+ line = "#{csi}"
291
+ next size = 0
295
292
  end
296
293
 
297
- if part == :nl
298
- lws ? yield(current.chop, width - 1) : yield(current, width)
299
- current = seq.dup
300
- width = 0
301
- next lws = false
294
+ if snipped == :hard_nl
295
+ yield(line, size)
296
+ line = EMPTY.dup
297
+ csi = nil
298
+ next size = 0
302
299
  end
303
300
 
304
- if part == :hard_nl
305
- lws ? yield(current.chop, width - 1) : yield(current, width)
306
- seq.clear
307
- current = EMPTY.dup
308
- width = 0
309
- next lws = false
301
+ if snipped == CsiEnd
302
+ line << CsiEnd if csi
303
+ next csi = nil
310
304
  end
311
305
 
312
- lws = false
306
+ next line << (csi = snipped) if snipped.is_a?(Csi)
307
+ next line << snipped if snipped.is_a?(Osc)
313
308
 
314
- if part == :seq
315
- current << opt
316
- next seq << opt
317
- end
318
-
319
- # :seq_end
320
- next if seq.empty?
321
- current << "\e[m"
322
- seq.clear
309
+ # Word:
310
+ line << snipped
311
+ size += snipped.size
323
312
  end
324
313
  nil
325
314
  end
326
315
 
327
- def pairs_in(text, bbcode, ansi, ignore_newline, limit)
328
- current = EMPTY.dup
329
- seq = EMPTY.dup
330
- width = 0
331
- lws = nil
332
- each_part(text, bbcode, ansi, ignore_newline) do |part, opt|
333
- if part == :space
334
- next if lws
335
- if width.succ < limit
336
- current << ' '
337
- width += 1
338
- next lws = true
339
- end
340
- yield(current, width)
341
- current = seq.dup
342
- width = 0
343
- next lws = false
316
+ def lines(text, bbcode, ansi, ignore_newline)
317
+ line = EMPTY.dup
318
+ csi = nil
319
+ as_snippeds(text, bbcode, ansi, ignore_newline, Word).each do |snipped|
320
+ next line << ' ' if snipped == :space
321
+
322
+ if snipped == :nl
323
+ next if line.empty?
324
+ yield(line)
325
+ next line = "#{csi}"
344
326
  end
345
327
 
346
- if part.is_a?(Word)
347
- if (nw = width + part.size) <= limit
348
- current << part
349
- width = nw
350
- next lws = false
351
- end
328
+ if snipped == :hard_nl
329
+ yield(line)
330
+ line = EMPTY.dup
331
+ next csi = nil
332
+ end
352
333
 
353
- if width > 0
354
- lws ? yield(current.chop, width - 1) : yield(current, width)
355
- end
356
- current = seq.dup
334
+ if snipped == CsiEnd
335
+ line << CsiEnd if csi
336
+ next csi = nil
337
+ end
357
338
 
358
- if part.size < limit
359
- current << part
360
- width = part.size
361
- next lws = false
362
- end
339
+ csi = snipped if snipped.is_a?(Csi)
340
+ # Csi, Osc, Word:
341
+ line << snipped
342
+ end
343
+ nil
344
+ end
363
345
 
364
- if part.size == limit
365
- yield(current + part, limit)
366
- current = seq.dup
367
- width = 0
368
- next lws = false
346
+ def as_snippeds(text, bbcode, ansi, ignore_newline, word_class)
347
+ ret = []
348
+ last = nil
349
+ text.each do |txt|
350
+ if (txt = bbcode ? Ansi.bbcode(txt) : txt.to_s).empty?
351
+ next ret[-1] = last = :hard_nl if ret[-1] == :space
352
+ next ret << (last = :hard_nl)
353
+ end
354
+ txt = txt.encode(ENC) if txt.encoding != ENC
355
+ txt.scan(SCAN_EXPR) do |nl, csi, osc, space, gc|
356
+ if gc
357
+ next last.add(gc, char_width(gc)) if last.is_a?(word_class)
358
+ next ret << (last = word_class.new(gc, char_width(gc)))
369
359
  end
370
-
371
- width = 0
372
- part.chars.each do |c, w|
373
- next current << c if (width += w) <= limit
374
- yield(current, width - w)
375
- current = seq.dup << c
376
- width = w
360
+ next last.is_a?(Symbol) ? nil : ret << (last = :space) if space
361
+ if nl
362
+ if ignore_newline
363
+ next last.is_a?(Symbol) ? nil : ret << (last = :space)
364
+ end
365
+ next last == :space ? ret[-1] = last = :nl : ret << (last = :nl)
377
366
  end
378
-
379
- next lws = false
367
+ next unless ansi
368
+ next ret << (last = Osc.new(osc)) if osc
369
+ next ret << (last = CsiEnd) if csi == "\e[m" || csi == "\e[0m"
370
+ last.is_a?(Csi) ? last.add(csi) : ret << (last = Csi.new(csi))
380
371
  end
372
+ next ret[-1] = last = :hard_nl if last.is_a?(Symbol)
373
+ ret << (last = :hard_nl)
374
+ end
375
+ ret
376
+ end
377
+ end
381
378
 
382
- if part == :nl
383
- lws ? yield(current.chop, width - 1) : yield(current, width)
384
- current = seq.dup
385
- width = 0
386
- next lws = false
387
- end
379
+ class Osc
380
+ attr_reader :to_str, :size
388
381
 
389
- if part == :hard_nl
390
- if width > 0
391
- lws ? yield(current.chop, width - 1) : yield(current, width)
392
- end
393
- seq.clear
394
- current = EMPTY.dup
395
- width = 0
396
- next lws = false
397
- end
382
+ alias to_s to_str
398
383
 
399
- lws = false
384
+ def initialize(str)
385
+ @to_str = str
386
+ @size = 0
387
+ end
400
388
 
401
- if part == :seq
402
- current << opt
403
- next seq << opt
404
- end
389
+ def inspect = "#{to_s.chop} #{@to_str.inspect}>"
390
+ end
405
391
 
406
- # :seq_end
407
- next if seq.empty?
408
- current << "\e[m"
409
- seq.clear
410
- end
411
- nil
412
- end
392
+ class Csi < Osc
393
+ def add(str) = (@to_str << str)
413
394
  end
414
395
 
415
- @ambiguous_char_width = 1
396
+ module CsiEnd
397
+ class << self
398
+ attr_reader :to_str, :size
399
+ end
400
+ @to_str = "\e[m"
401
+ @size = 0
402
+ end
416
403
 
417
404
  class Word
418
- attr_reader :to_str, :size, :chars
405
+ attr_reader :to_str, :size
406
+
407
+ alias to_s to_str
419
408
 
420
409
  def initialize(char, size)
421
410
  @to_str = char.dup
422
411
  @size = size
423
- @chars = [[char, size]]
424
412
  end
425
413
 
426
414
  def add(char, size)
427
415
  @to_str << char
428
416
  @size += size
417
+ end
418
+
419
+ def inspect = "#{to_s.chop} #{@size}:#{@to_str.inspect}>"
420
+ end
421
+
422
+ class WordEx < Word
423
+ attr_reader :chars
424
+
425
+ def initialize(char, size)
426
+ super
427
+ @chars = [[char, size]]
428
+ end
429
+
430
+ def add(char, size)
429
431
  @chars << [char, size]
430
- nil
432
+ super
433
+ end
434
+
435
+ def split(limit)
436
+ chars = @chars.dup
437
+ ret = [last = Word.new(*chars.shift)]
438
+ chars.each do |c, s|
439
+ next ret << (last = Word.new(c, s)) if last.size + s > limit
440
+ last.add(c, s)
441
+ end
442
+ ret
431
443
  end
432
444
  end
433
- private_constant :Word
445
+
446
+ private_constant :Osc, :CsiEnd, :Csi, :Word, :WordEx
447
+
448
+ @ambiguous_char_width = 1
434
449
 
435
450
  ENC = Encoding::UTF_8
436
451
  EMPTY = String.new(encoding: ENC).freeze
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Terminal
4
4
  # The version number of the gem.
5
- VERSION = '0.9.1'
5
+ VERSION = '0.9.4'
6
6
  end
data/lib/terminal.rb CHANGED
@@ -6,16 +6,21 @@ require_relative 'terminal/input'
6
6
  #
7
7
  # Terminal access with support for ANSI control codes and
8
8
  # [BBCode-like](https://en.wikipedia.org/wiki/BBCode) embedded text attribute
9
- # syntax.
9
+ # syntax (see {Ansi.bbcode}).
10
10
  #
11
- # It automagically detects whether your terminal supports ANSI features.
11
+ # It automagically detects whether your terminal supports ANSI features, like
12
+ # coloring (see {colors}) or the
13
+ # [CSIu protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol) support
14
+ # (see {read_key} and {input_mode}). It calculates the display width for Unicode
15
+ # chars (see {Text.width}) and help you to display text with line formatting
16
+ # (see {Text.each_line}).
12
17
  #
13
18
  module Terminal
14
19
  class << self
15
20
  # Return true when the current terminal supports ANSI control codes.
16
21
  # When the terminal does not support it, {colors} will return `2` as color
17
22
  # count and all output methods ({<<}, {print}, {puts}) will not forward
18
- # ANSI control codes to the terminal.
23
+ # ANSI control codes to the terminal, {read_key} will not support CSIu.
19
24
  #
20
25
  # @attribute [r] ansi?
21
26
  # @return [Boolean] whether ANSI control codes are supported
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: terminal_rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.1
4
+ version: 0.9.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Blumtritt
@@ -34,6 +34,8 @@ files:
34
34
  - lib/terminal/ansi/named_colors.rb
35
35
  - lib/terminal/detect.rb
36
36
  - lib/terminal/input.rb
37
+ - lib/terminal/input/csiu_mode.rb
38
+ - lib/terminal/input/legacy_mode.rb
37
39
  - lib/terminal/preload.rb
38
40
  - lib/terminal/rspec/helper.rb
39
41
  - lib/terminal/text.rb
@@ -47,7 +49,7 @@ metadata:
47
49
  rubygems_mfa_required: 'true'
48
50
  source_code_uri: https://codeberg.org/mblumtritt/Terminal.rb
49
51
  bug_tracker_uri: https://codeberg.org/mblumtritt/Terminal.rb/issues
50
- documentation_uri: https://rubydoc.info/gems/terminal_rb/0.9.1/Terminal
52
+ documentation_uri: https://rubydoc.info/gems/terminal_rb/0.9.4/Terminal
51
53
  rdoc_options: []
52
54
  require_paths:
53
55
  - lib