terminal_rb 1.0.8 → 1.0.9

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 91c150887b2ab21e509a1d8da160bc89879d9750de3246a2f84ee0c87c5a3992
4
- data.tar.gz: 640cc99db38489ca6ccb57f3d7605fdc199a581ff674f20cda145b189a62747c
3
+ metadata.gz: 8a5e3a8b375cd45eef93b2316251d14da8732b80e27fb1192bbf8fb87a9bf297
4
+ data.tar.gz: f10bcbf23b3e8ecd01bc51a8772f2099dbe5950bb561bc26d854e6c3d2fb382d
5
5
  SHA512:
6
- metadata.gz: 9a2f57d900145e027ea44bad3bea885a46c37c451e31cd9c0623c0d4935022ab3eb3479a76095a094d0ed3acb91d4248c2052d7b635538cf95d540af3a2b8512
7
- data.tar.gz: 17b15d64bb67d748202fe7db329565d9bce197c50c00bb6005e15483acea2461d59b11f16d9910c4092532299c49047817d9e4934d54b8e8d59f1580040d4314
6
+ metadata.gz: a3c0cbfe105fccd17923c6ba7f43faf797b2df1fcc5d76d8bf138b1593156c5e23dc2a9700be17fa61e8f744151bb756943a482f9eca501087ffabb257e7e608
7
+ data.tar.gz: 60616eb76bbc7d01e1759274220390611887bbafe12f2d3b7cda8eb68dcd6cee491ce6b477235ce9a8b5e43b372e6d3e6c7a8ff81962a67eb78a20cb58fe7523
data/README.md CHANGED
@@ -1,10 +1,10 @@
1
- # Terminal.rb v1.0.8
1
+ # Terminal.rb v1.0.9
2
2
 
3
3
  Terminal.rb supports you with input and output on your terminal. Simple [BBCode](https://en.wikipedia.org/wiki/BBCode)-like markup for attributes and coloring, word-wise line breaks, correct special key recognition and mouse event reporting enable you to implement your CLI app quickly and easily.
4
4
 
5
5
  - Gem: [rubygems.org](https://rubygems.org/gems/terminal_rb)
6
6
  - Source: [codeberg.org](https://codeberg.org/mblumtritt/Terminal.rb)
7
- - Help: [rubydoc.info](https://rubydoc.info/gems/terminal_rb/1.0.8/Terminal)
7
+ - Help: [rubydoc.info](https://rubydoc.info/gems/terminal_rb/1.0.9/Terminal)
8
8
 
9
9
  ## Features
10
10
 
data/examples/bbcode.rb CHANGED
@@ -6,28 +6,28 @@ Terminal.puts <<~TEXT
6
6
 
7
7
  ✅ [b bright_green]Terminal.rb::Ansi[/b] — BBCode:[/]
8
8
 
9
- [b]Bold[/b] [\\b][d]...[/d][\\/b] [\\bold][d] ... [/d][\\/bold]
10
- [dim]Dim[/dim] [\\d][d]...[/d][\\/d] [\\dim][d] ... [/d][\\/dim]
11
- [i]Italic[/i] [\\i][d]...[/d][\\/i] [\\italic][d] ... [/d][\\/italic]
9
+ [b]Bold[/b] [\\b][d]...[/d][\\/b] [\\bold][d]........[/d][\\/bold]
10
+ [dim]Dim[/dim] [\\d][d]...[/d][\\/d] [\\dim][d].........[/d][\\/dim]
11
+ [i]Italic[/i] [\\i][d]...[/d][\\/i] [\\italic][d]......[/d][\\/italic]
12
12
  [u]Underline[/u] [\\u][d]...[/d][\\/u] [\\underline][d]...[/d][\\/underline]
13
- [h]Hide[/h] [\\h][d]...[/d][\\/h] [\\hide][d] ... [/d][\\/hide]
13
+ [h]Hide[/h] [\\h][d]...[/d][\\/h] [\\hide][d]........[/d][\\/hide]
14
14
 
15
- [inv]Invert[/inv] [\\inv][d] ... [/d][\\/inv] [\\invert][d]...[/d][\\/invert]
15
+ [inv]Invert[/inv] [\\inv][d]......[/d][\\/inv] [\\invert][d]...[/d][\\/invert]
16
16
  [strike]Strike[/strike] [\\strike][d]...[/d][\\/strike]
17
- [blink]Blink[/blink] [\\blink][d] ...[/d][\\/blink]
17
+ [blink]Blink[/blink] [\\blink][d]....[/d][\\/blink]
18
18
 
19
- [u]Underline[/u] [\\u][d] ... [/d][\\/u] [\\underline][d] ... [/d][\\/underline]
20
- [uu]Double underline[/uu] [\\uu][d] ...[/d][\\/uu] [\\double_underline][d]...[/d][\\/double_underline]
21
- [cu]Curly underline[/cu] [\\cu][d] ...[/d][\\/cu] [\\curly_underline][d] ...[/d][\\/curly_underline]
19
+ [u]Underline[/u] [\\u][d].....[/d][\\/u] [\\underline][d]..........[/d][\\/underline]
20
+ [uu]Double underline[/uu] [\\uu][d]....[/d][\\/uu] [\\double_underline][d]...[/d][\\/double_underline]
21
+ [cu]Curly underline[/cu] [\\cu][d]....[/d][\\/cu] [\\curly_underline][d]...[/d] [\\/curly_underline]
22
22
  [dau]Dashed underline[/dau] [\\dau][d]...[/d][\\/dau] [\\dashed_underline][d]...[/d][\\/dashed_underline]
23
23
  [dou]Dotted underline[/dou] [\\dou][d]...[/d][\\/dou] [\\dotted_underline][d]...[/d][\\/dotted_underline]
24
24
 
25
- [fraktur]Fraktur[/fraktur] [\\fraktur][d] ... [/d][\\/fraktur]
26
- [framed]Framed[/framed] [\\framed][d] ... [/d][\\/framed]
25
+ [fraktur]Fraktur[/fraktur] [\\fraktur][d].....[/d][\\/fraktur]
26
+ [framed]Framed[/framed] [\\framed][d]......[/d][\\/framed]
27
27
  [encircled]Encircled[/encircled] [\\encircled][d]...[/d][\\/encircled]
28
28
 
29
- [ovr]Overlined[/ovr] [\\ovr][d]...[/d][\\/ovr] [\\overlined][d] ... [/d][\\/overlined]
30
- [sub]Subscript[/sub] [\\sub][d]...[/d][\\/sub] [\\subscript][d] ... [/d][\\/subscript]
29
+ [ovr]Overlined[/ovr] [\\ovr][d]...[/d][\\/ovr] [\\overlined][d].....[/d][\\/overlined]
30
+ [sub]Subscript[/sub] [\\sub][d]...[/d][\\/sub] [\\subscript][d].....[/d][\\/subscript]
31
31
  [sup]Superscript[/sup] [\\sup][d]...[/d][\\/sup] [\\superscript][d]...[/d][\\/superscript]
32
32
 
33
33
  TEXT
data/lib/terminal/ansi.rb CHANGED
@@ -16,7 +16,7 @@ module Terminal
16
16
  # Terminal::Ansi[:on_blue] # => "\e[44m"
17
17
  #
18
18
  # @example BBCode to ANSI
19
- # Terminal::Ansi.bbcode('[bold]Hello[/bold]') # => "\e[1mHello\e[m"
19
+ # Terminal::Ansi.bbcode('[bold]Hello[/bold]') # => "\e[1mHello\e[22m"
20
20
  #
21
21
  module Ansi
22
22
  class << self
@@ -39,7 +39,7 @@ module Terminal
39
39
  def named_colors = NAMED_COLORS.keys.map!(&:to_sym)
40
40
 
41
41
  #
42
- # @!group ANSI Control Code Generation
42
+ # @!group Text Attribute Code Generation
43
43
  #
44
44
 
45
45
  # Generate an ANSI escape sequence from attribute names or color
@@ -67,20 +67,12 @@ module Terminal
67
67
  "\e[#{
68
68
  attributes
69
69
  .map! do |arg|
70
- case arg
71
- when String
72
- @attr_str_map[arg] || _invalid(arg)
73
- when Symbol
74
- @attr_sym_map[arg] || _invalid(arg)
75
- when (0..255)
76
- "38;5;#{arg}"
77
- when (256..511)
78
- "48;5;#{arg - 256}"
79
- when (512..767)
80
- "58;5;#{arg - 512}"
81
- else
82
- _invalid(arg)
83
- end
70
+ next @attr_str_map[arg] || _invalid(arg) if arg.is_a?(String)
71
+ next @attr_sym_map[arg] || _invalid(arg) if arg.is_a?(Symbol)
72
+ _invalid(arg) unless arg.is_a?(Integer)
73
+ next "38;5;#{arg}" if arg >= 0 && arg <= 255
74
+ next "48;5;#{arg - 256}" if arg >= 256 && arg <= 511
75
+ arg >= 512 && arg <= 767 ? "58;5;#{arg - 512}" : _invalid(arg)
84
76
  end
85
77
  .join(';')
86
78
  }m"
@@ -130,10 +122,7 @@ module Terminal
130
122
  # attribute is invalid
131
123
  def try_convert(attributes, separator: ' ')
132
124
  return unless attributes
133
- cache = @convert_cache[separator]
134
- hit = cache[key = attributes.to_s] and return hit
135
- cache.clear if cache.size > 1024 # cache limit
136
- cache[key] = unless (attributes = key.split(separator)).empty?
125
+ unless (attributes = attributes.to_s.split(separator)).empty?
137
126
  # attributes.uniq!
138
127
  codes = attributes.map { @attr_str_map[it] || break }
139
128
  "\e[#{codes.join(';')}m" if codes
@@ -168,7 +157,7 @@ module Terminal
168
157
  #
169
158
  # @!endgroup
170
159
  #
171
- # @!group BBcode Generation
160
+ # @!group BBCode Generation
172
161
  #
173
162
 
174
163
  # Convert BBCode markup to ANSI escape codes.
@@ -191,11 +180,16 @@ module Terminal
191
180
  def bbcode(str)
192
181
  str = str.to_s
193
182
  return str.dup unless str.index('[')
194
- str.gsub(@re_bbcode) do |match_str|
195
- match = Regexp.last_match(1) or next match_str
196
- next try_convert(match) || match_str if match[0] != '\\'
197
- match[0] = ''
198
- "[#{match}]"
183
+ str.gsub(@re_bbcode) do |match|
184
+ hit = @bbcc[match] and next hit
185
+ next "[#{match[2..]}" if (fc = match[1]) == '\\'
186
+ @bbcc.clear if @bbcc.size == 1024
187
+ @bbcc[match] = if fc == ']' || (att = match[1..-2].split).empty?
188
+ match
189
+ else
190
+ att = att.map { @attr_str_map[it] || break }
191
+ att ? "\e[#{att.join(';')}m" : match
192
+ end
199
193
  end
200
194
  end
201
195
 
@@ -211,15 +205,12 @@ module Terminal
211
205
  def unbbcode(str)
212
206
  str = str.to_s
213
207
  return str.dup unless str.index('[')
214
- str.gsub(@re_bbcode) do |match_str|
215
- match = Regexp.last_match(1) or next match_str
216
- if match[0] == '\\'
217
- match[0] = ''
218
- next "[#{match}]"
219
- end
220
- next match_str if (match = match.split).empty?
221
- next if match.all? { @attr_str_map[it] }
222
- match_str
208
+ str.gsub(@re_bbcode) do |match|
209
+ hit = @bbcc[match] and next hit[0] == '[' ? hit : nil
210
+ next match if (fc = match[1]) == ']'
211
+ next "[#{match[2..]}" if fc == '\\'
212
+ next match if (att = match[1..-2].split).empty?
213
+ match unless att.map { @attr_str_map[it] || break }
223
214
  end
224
215
  end
225
216
 
@@ -237,13 +228,11 @@ module Terminal
237
228
  def escape_bbcode(str)
238
229
  str = str.to_s
239
230
  return str.dup unless str.index('[')
240
- str.gsub(@re_bbcode) do |match_str|
241
- fc = match_str[1]
242
- next match_str if fc == '\\' || fc == ']'
243
- match = Regexp.last_match(1) or next match_str
244
- next match_str if (parts = match.split).empty?
245
- next "[\\#{match}]" if parts.all? { @attr_str_map[it] }
246
- match_str
231
+ str.gsub(@re_bbcode) do |match|
232
+ hit = @bbcc[match] and hit[1] == '\\' ? hit : "[\\#{match[1..]}"
233
+ next match if (fc = match[1]) == '\\' || fc == ']'
234
+ next match if (att = match[1..-2].split).empty?
235
+ att.map { @attr_str_map[it] || break } ? "[\\#{match[1..]}" : match
247
236
  end
248
237
  end
249
238
 
@@ -274,14 +263,9 @@ module Terminal
274
263
  # @return [true, false]
275
264
  def valid?(*attributes)
276
265
  attributes.all? do |arg|
277
- case arg
278
- when String
279
- @attr_str_map[arg]
280
- when Symbol
281
- @attr_sym_map[arg]
282
- when (0..767)
283
- true
284
- end
266
+ next @attr_str_map[arg] if arg.is_a?(String)
267
+ next @attr_sym_map[arg] if arg.is_a?(Symbol)
268
+ arg.is_a?(Integer) && arg >= 0 && arg <= 767
285
269
  end
286
270
  end
287
271
 
@@ -303,7 +287,7 @@ module Terminal
303
287
  #
304
288
  # @!endgroup
305
289
  #
306
- # @!group ANSI Control Code Generation
290
+ # @!group Cursor and Screen Control Codes
307
291
  #
308
292
 
309
293
  # Move cursor up.
@@ -458,54 +442,26 @@ module Terminal
458
442
  # @return (see .[])
459
443
  def line_erase(part = :all) = "\e[#{@line_erase[part]}K"
460
444
 
461
- # Set the terminal window title.
462
- # @note Supported by Hyper, iTerm2, Kitty, macOS Terminal, Tabby,
463
- # WezTerm.
464
- #
465
- # @example
466
- # Terminal.raw_write(Terminal::Ansi.title('My App'))
467
- #
468
- # @param title [String] the title text
469
- # @return (see .[])
470
- def title(title) = "\e]0;#{title}\a"
471
-
472
- # Start a hyperlink (OSC 8).
473
445
  #
474
- # @note Supported by Ghostty, iTerm2, Kitty, Rio, Tabby, WezTerm.
475
- #
476
- # @see .link
477
- # @see .link_end
478
- #
479
- # @param url [#to_s] the link URL
480
- # @param params [Hash] optional link parameters (e.g., `id:`)
481
- # @return (see .[])
482
- def link_start(url, **params)
483
- "\e]8;#{params.map { it.join('=') }.join(':')};#{url}\a"
484
- end
485
-
486
- # End a hyperlink.
446
+ # @!endgroup
487
447
  #
488
- # @see .link_start
448
+ # @!group Terminal Integration Codes
489
449
  #
490
- # @return (see .[])
491
- def link_end = +LINK_END
492
450
 
493
- # Create a complete hyperlink (OSC 8).
451
+ # Set the terminal window title.
494
452
  #
495
- # @see .link_start
496
- # @see .link_end
453
+ # @note Supported by iTerm2, Kitty, macOS Terminal, Tabby,
454
+ # WezTerm.
497
455
  #
498
456
  # @example
499
- # Terminal::Ansi.link('https://example.com', 'Example')
457
+ # Terminal.raw_write(Terminal::Ansi.title('My App'))
500
458
  #
501
- # @param (see .link_start)
502
- # @param text [String] the visible link text
459
+ # @param title [#to_s] the title text
503
460
  # @return (see .[])
504
- def link(url, text, **params)
505
- "#{link_start(url, **params)}#{text}#{LINK_END}"
506
- end
461
+ def title(title) = "\e]0;#{_no_bell(title)}\a"
507
462
 
508
463
  # Send a desktop notification via the terminal.
464
+ #
509
465
  # @note Supported by Ghostty, iTerm2, Kitty, WezTerm.
510
466
  #
511
467
  # @example
@@ -513,7 +469,7 @@ module Terminal
513
469
  #
514
470
  # @param text [to_s] the notification text
515
471
  # @return (see .[])
516
- def notify(text) = "\e]9;#{text}\a"
472
+ def notify(text) = "\e]9;#{_no_bell(text)}\a"
517
473
 
518
474
  # Show or update a progress indicator in the terminal tab/title
519
475
  # bar.
@@ -550,10 +506,100 @@ module Terminal
550
506
  "\e]9;4;#{state};#{percent.to_i.clamp(0, 100)}\a"
551
507
  end
552
508
 
509
+ # @comment Should we implement this?
510
+ #
511
+ # # Request attention.
512
+ # #
513
+ # # @note Supported by iTerm2.
514
+ # #
515
+ # # @param action [true, :bounce, :once, :fireworks, false, :stop]
516
+ # # start bouncing dock icon (`true`, `:bounce`),
517
+ # # bounce dock icon once (`:once`)
518
+ # # or stop attention (`false`, `:stop`)
519
+ # # @return (see .[])
520
+ # def attention(action = :bounce)
521
+ # "\e]1337;RequestAttention=#{
522
+ # case action
523
+ # when true, :bounce
524
+ # 'yes'
525
+ # when :once, :fireworks
526
+ # action
527
+ # else
528
+ # 'no'
529
+ # end
530
+ # }\a"
531
+ # end
532
+
533
+ # Create or start a hyperlink.
534
+ #
535
+ # @note Supported by Ghostty, iTerm2, Kitty, Rio, Tabby, WezTerm.
536
+ #
537
+ # @see .link_end
538
+ #
539
+ # @example
540
+ # Terminal::Ansi.link('https://example.com', 'Example')
541
+ #
542
+ # @param url [#to_s] the link URL
543
+ # @param text [nil, #to_s] the visible link text
544
+ # @param params [Hash] optional link parameters (e.g., `id:`)
545
+ # @return (see .[])
546
+ def link(url, text = nil, **params)
547
+ start =
548
+ "\e]8;#{
549
+ _no_bell(params.map { it.join('=') }.join(':'))
550
+ };#{_no_bell(url)}\a"
551
+ text ? "#{start}#{_no_bell(text)}#{LINK_END}" : start
552
+ end
553
+
554
+ # End a hyperlink.
555
+ #
556
+ # @see .link
557
+ #
558
+ # @return (see .[])
559
+ def link_end = +LINK_END
560
+
561
+ # Copy text to clipboard.
562
+ #
563
+ # @note Supported by Ghostty, iTerm2, Kitty, Rio, Tabby, WezTerm.
564
+ #
565
+ # @param text [#to_s] text to copy
566
+ # @return (see .[])
567
+ def clipboard(text) = "\e]52;c;#{[text.to_s].pack('m0')}\a"
568
+
569
+ # Show or update the session status of the terminal.
570
+ #
571
+ # @note Supported by iTerm2.
572
+ #
573
+ # @example
574
+ # Terminal.raw_write(Terminal::Ansi.status(text: 'Working'))
575
+ # Terminal.raw_write(Terminal::Ansi.status(indicator: '#fa00fa'))
576
+ # Terminal.raw_write(Terminal::Ansi.status(indicator: :f00))
577
+ # Terminal.raw_write(Terminal::Ansi.status) # reset the status
578
+ #
579
+ # @param text [nil, #to_s] status text
580
+ # @param detail [nil, #to_s] status detail text
581
+ # @param color [nil, String, Symbol] text color
582
+ # @param indicator [nil, String, Symbol] status indicator color
583
+ # @return (see .[])
584
+ def status(text: nil, detail: nil, color: nil, indicator: nil)
585
+ str = text ? ";status=#{_no_bell(text)}" : ''
586
+ indicator = _try_rgb(indicator) and str << ";indicator=#{indicator}"
587
+ color = _try_rgb(color) and str << ";color=#{color}"
588
+ str << "detail=#{_no_bell(detail)}" if detail
589
+ str.empty? ? +STATUS_RESET : "\e]21337#{str}\a"
590
+ end
591
+
592
+ # Reset session status of the terminal.
593
+ #
594
+ # @see .status
595
+ #
596
+ # @return (see .[])
597
+ def status_reset = +STATUS_RESET
598
+
553
599
  # Scale text using the
554
600
  # [text sizing protocol](https://sw.kovidgoyal.net/kitty/text-sizing-protocol).
555
601
  #
556
- # @note Only supported by Kitty.
602
+ # @note Supported by Kitty.
557
603
  #
558
604
  # @example Double-height Greeting
559
605
  # Terminal::Ansi.scale('Hello Ruby!', scale: 2)
@@ -619,16 +665,23 @@ module Terminal
619
665
  caller(1)
620
666
  )
621
667
  end
668
+
669
+ def _try_rgb(str)
670
+ mt = /\A?#?([[:xdigit:]]{3,6})\z/.match(str.to_s)&.[](1) or return
671
+ "##{mt.size == 6 ? mt : mt[..2]}"
672
+ end
673
+
674
+ def _no_bell(str) = str.to_s.tr("\a", '')
622
675
  end
623
676
 
624
677
  @re_test =
625
678
  /
626
- (?:\e\[[\x30-\x3f]*[\x20-\x2f]*[a-zA-Z])
627
- |
628
- (?:\e\]\d+(?:;[^\a\e]+)*(?:\a|\e\\))
629
- /x
679
+ (?:\e\[[\x30-\x3f]*[\x20-\x2f]*[a-zA-Z])
680
+ | (?:\e\]\d+(?:;[^\a\e]+)*(?:\a|\e\\))
681
+ /x
630
682
 
631
- @re_bbcode = /(?:\[((?~[\[\]]))\])/
683
+ @re_bbcode_old = /(?:\[((?~[\[\]]))\])/
684
+ @re_bbcode = /(?:\[(?~[\[\]]))\]/
632
685
 
633
686
  color_map = {
634
687
  # foreground
@@ -818,13 +871,12 @@ module Terminal
818
871
  b = /\A(on|ul)?_?([a-z]{3,}[0-9]{0,3})\z/.match(s) or next h[s] = nil
819
872
  next h[s] = (v = NAMED_COLORS[b[2]]) ? "#{@cbase[b[1]]};#{v}" : nil
820
873
  end
821
- h[s] = case v.size
822
- when 1, 2
823
- "#{@cbase[b]};5;#{v.hex}"
824
- when 3
825
- "#{@cbase[b]};2;#{(v[0] * 2).hex};#{(v[1] * 2).hex};#{(v[2] * 2).hex}"
826
- when 6
874
+ h[s] = if v.size == 6
827
875
  "#{@cbase[b]};2;#{v[0, 2].hex};#{v[2, 2].hex};#{v[4, 2].hex}"
876
+ elsif v.size == 3
877
+ "#{@cbase[b]};2;#{(v[0] * 2).hex};#{(v[1] * 2).hex};#{(v[2] * 2).hex}"
878
+ elsif v.size < 3
879
+ "#{@cbase[b]};5;#{v.hex}"
828
880
  end
829
881
  end
830
882
 
@@ -832,7 +884,7 @@ module Terminal
832
884
  @attr_sym_map.compare_by_identity
833
885
  @attr_sym_map.default_proc = proc { |h, s| h[s] = @attr_str_map[s.to_s] }
834
886
 
835
- @convert_cache = Hash.new { |h, k| h[k] = {} }
887
+ @bbcc = {} # BBCode cache
836
888
 
837
889
  @screen_erase = { below: nil, above: '1', scrollback: '3' }
838
890
  @screen_erase.default = '2'
@@ -917,6 +969,16 @@ module Terminal
917
969
  # @private
918
970
  LINE_ERASE_PREV = -"#{cursor_prev_line(nil)}#{LINE_ERASE}"
919
971
 
972
+ # @comment Supported by Terminal, iTerm, WezTerm:
973
+ # @private
974
+ LINE_DOUBLE_HEIGHT_TOP = "\e#3"
975
+ # @private
976
+ LINE_DOUBLE_HEIGHT_BOTTOM = "\e#4"
977
+ # @private
978
+ LINE_SINGLE_WIDTH = "\e#5"
979
+ # @private
980
+ LINE_DOUBLE_WIDTH = "\e#6"
981
+
920
982
  # @private
921
983
  LINK_END = "\e]8;;\a"
922
984
 
@@ -925,6 +987,9 @@ module Terminal
925
987
  # @private
926
988
  PROGRESS_SHOW_INDETERMINATE = "\e]9;4;3;0\a"
927
989
 
990
+ # @private
991
+ STATUS_RESET = "\e]21337;status=;indicator=;status-color=\a"
992
+
928
993
  # @comment seems not widely supported:
929
994
  # doubled: def cursor_column(column = 1) = "\e[#{column}`"
930
995
  # doubled: def cursor_row(row = 1) = "\e[#{row}d"
@@ -942,13 +1007,6 @@ module Terminal
942
1007
  #
943
1008
  # def set_scroll_region(top = nil, bottom = nil) = "\e[#{top};#{bottom}r"
944
1009
 
945
- # @comment other:
946
- # "\eE" same as "\r\n"
947
- # "\eD" same as "\n" but preserves column
948
- # "\eM" reverse "\n"
949
- # "\e[6n" report Cursor Position as "ESC \[ row ; col R"
950
- # "\e[21t report window’s title as ESC ] l title ESC \"
951
-
952
1010
  # @comment TODO:
953
1011
  # https://sw.kovidgoyal.net/kitty/desktop-notifications
954
1012
  # https://sw.kovidgoyal.net/kitty/pointer-shapes
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Terminal
4
4
  # @private
5
- module DumpInput
5
+ module DumbInput
6
6
  def input_mode = :dumb
7
7
 
8
8
  def on_key_event(*)
@@ -32,5 +32,5 @@ module Terminal
32
32
  end
33
33
  end
34
34
 
35
- private_constant :DumpInput
35
+ private_constant :DumbInput
36
36
  end
@@ -9,9 +9,9 @@ module Terminal
9
9
  # @see Terminal.read_key_event
10
10
  #
11
11
  # @example Parse a raw escape sequence
12
- # event = Terminal::KeyEvent["\r"]
13
- # event.key # => :Enter
14
- # event.name # => "Enter"
12
+ # event = Terminal::KeyEvent["a"]
13
+ # event.key # => "a"
14
+ # event.name # => "a"
15
15
  # event.simple? # => true
16
16
  #
17
17
  # @example Inspect modifier keys
@@ -33,7 +33,7 @@ module Terminal
33
33
  # @see #modifiers
34
34
  # @see #modifier?
35
35
  #
36
- # @return [Integer] modifier_bits bitmask
36
+ # @return [Integer] bitmask of the active modifiers
37
37
  attr_reader :modifier_bits
38
38
 
39
39
  # Names of active modifier keys.
@@ -14,7 +14,6 @@ module Terminal
14
14
  # @example
15
15
  # Terminal.input_mode # => :csi_u
16
16
  #
17
- # @attribute [r] input_mode
18
17
  # @return [Symbol] `:csi_u`, `:legacy`, `:dumb`, or `:error`
19
18
 
20
19
  #
@@ -33,7 +32,7 @@ module Terminal
33
32
  #
34
33
  # @example
35
34
  # Terminal.on_key_event(mouse: true) do |event|
36
- # break if event.name == 'ESC'
35
+ # break if event.name == 'Esc'
37
36
  # puts event.name
38
37
  # end
39
38
  #
@@ -69,7 +68,7 @@ module Terminal
69
68
  extend AnsiInput
70
69
  when :dumb
71
70
  require_relative('input/dumb')
72
- extend DumpInput
71
+ extend DumbInput
73
72
  else
74
73
  __input_error
75
74
  end
@@ -88,19 +88,13 @@ module Terminal
88
88
  objects.flatten!
89
89
  return @out.puts if objects.empty?
90
90
  @out.puts(
91
- Text::Formatter.format(
91
+ Text::Formatter.new(
92
92
  *objects,
93
93
  ansi: true,
94
94
  bbcode:,
95
95
  spaces:,
96
- eol:,
97
- align:,
98
- width:,
99
- height:,
100
- padding:,
101
- prefix:,
102
- suffix:
103
- )
96
+ eol:
97
+ ).format(align:, width:, height:, padding:, prefix:, suffix:)
104
98
  )
105
99
  rescue IOError, SystemCallError
106
100
  __output_error(nil)
@@ -139,16 +133,17 @@ module Terminal
139
133
  require('io/console')
140
134
  @con = IO.console
141
135
  return unless Signal.list.key?('WINCH')
142
- Signal.trap('WINCH') do
143
- @size = nil
144
- @on_resize&.call
145
- end
136
+ prev_winch =
137
+ Signal.trap('WINCH') do
138
+ @size = nil
139
+ @on_resize&.call
140
+ prev_winch.call if prev_winch.respond_to?(:call)
141
+ end
146
142
  end
147
143
 
148
144
  private_class_method def self.extended(mod)
149
145
  mod.instance_variable_set(:@cc, 0)
150
146
  mod.instance_variable_set(:@as, 0)
151
- # at_exit { raw_write("#{Ansi::CURSOR_SHOW}#{Ansi::SCREEN_ALTERNATE_OFF}") }
152
147
  end
153
148
  end
154
149
 
@@ -69,19 +69,13 @@ module Terminal
69
69
  objects.flatten!
70
70
  return @out.puts if objects.empty?
71
71
  @out.puts(
72
- Text::Formatter.format(
72
+ Text::Formatter.new(
73
73
  *objects,
74
74
  ansi: false,
75
75
  bbcode:,
76
76
  spaces:,
77
- eol:,
78
- align:,
79
- width:,
80
- height:,
81
- padding:,
82
- prefix:,
83
- suffix:
84
- )
77
+ eol:
78
+ ).format(align:, width:, height:, padding:, prefix:, suffix:)
85
79
  )
86
80
  rescue IOError, SystemCallError
87
81
  __output_error(nil)
@@ -27,7 +27,7 @@ module Terminal
27
27
  # @!group Output Attributes
28
28
  #
29
29
 
30
- # @!attribute [r] tui?
30
+ # @attribute [r] tui?
31
31
  #
32
32
  # Whether the terminal supports full TUI interaction (ANSI output
33
33
  # with CSI-u or legacy keyboard input).
@@ -36,7 +36,7 @@ module Terminal
36
36
  #
37
37
  # @return [true, false]
38
38
 
39
- # @!attribute [r] ansi?
39
+ # @attribute [r] ansi?
40
40
  #
41
41
  # Whether ANSI escape codes are supported.
42
42
  #
@@ -181,7 +181,6 @@ module Terminal
181
181
  # @!method show_cursor
182
182
  # Show the cursor.
183
183
  #
184
- #
185
184
  # @note Reference-counted; safe for nested use.
186
185
  #
187
186
  # @see .hide_cursor
@@ -4,9 +4,10 @@ require 'stringio'
4
4
 
5
5
  # RSpec shared context for testing code that uses Terminal.rb.
6
6
  #
7
- # Stubs all Terminal I/O methods and provides `stdout`, `stdin`, and
8
- # `stdoutstr` test helpers. Output is captured into the `stdout` array;
9
- # input is read from the `stdin` array.
7
+ # Stubs all Terminal I/O methods and provides the `stdout`, `stdout_lines`,
8
+ # and `stdin` test helpers. Output is captured in the `stdout` StringIO;
9
+ # `stdout_lines` returns its lines without line endings. Input is read from
10
+ # the `stdin` array of raw key sequences.
10
11
  #
11
12
  # @example Basic usage
12
13
  # RSpec.describe MyClass do
@@ -14,7 +15,7 @@ require 'stringio'
14
15
  #
15
16
  # it 'prints a greeting' do
16
17
  # my_object.greet
17
- # expect(stdoutstr).to include('Hello')
18
+ # expect(stdout.string).to include('Hello')
18
19
  # end
19
20
  # end
20
21
  #
@@ -3,7 +3,7 @@
3
3
  module Terminal
4
4
  module Text
5
5
  # supported Unicode Standard version
6
- UNICODE_VERSION = '17.0.0'
6
+ UNICODE_VERSION = '18.0.0'
7
7
 
8
8
  #
9
9
  # This lookup was generated based on
@@ -11,7 +11,6 @@ module Terminal
11
11
  #
12
12
  CharWidth =
13
13
  Hash.new do |h, ord|
14
- next h[ord] = 1 if ord < 0xa1
15
14
  h[ord] = @char_width[@char_width_last.bsearch_index { ord <= it }] ||
16
15
  @ambiguous_char_width
17
16
  end
@@ -163,7 +162,7 @@ module Terminal
163
162
  0x5c3,
164
163
  0x5c5,
165
164
  0x5c6,
166
- 0x5c7,
165
+ 0x5c9,
167
166
  0x5ff,
168
167
  0x605,
169
168
  0x60f,
@@ -276,7 +275,7 @@ module Terminal
276
275
  0xb44,
277
276
  0xb4c,
278
277
  0xb4d,
279
- 0xb54,
278
+ 0xb52,
280
279
  0xb56,
281
280
  0xb61,
282
281
  0xb63,
@@ -441,9 +440,7 @@ module Terminal
441
440
  0x1a7e,
442
441
  0x1a7f,
443
442
  0x1aaf,
444
- 0x1add,
445
- 0x1adf,
446
- 0x1aeb,
443
+ 0x1af0,
447
444
  0x1aff,
448
445
  0x1b03,
449
446
  0x1b33,
@@ -956,7 +953,9 @@ module Terminal
956
953
  0x10d6d,
957
954
  0x10eaa,
958
955
  0x10eac,
959
- 0x10ef9,
956
+ 0x10ec8,
957
+ 0x10ecf,
958
+ 0x10eef,
960
959
  0x10eff,
961
960
  0x10f45,
962
961
  0x10f50,
@@ -1146,6 +1145,8 @@ module Terminal
1146
1145
  0x11d95,
1147
1146
  0x11d96,
1148
1147
  0x11d97,
1148
+ 0x11def,
1149
+ 0x11df0,
1149
1150
  0x11ef2,
1150
1151
  0x11ef4,
1151
1152
  0x11eff,
@@ -1180,11 +1181,15 @@ module Terminal
1180
1181
  0x16fef,
1181
1182
  0x16ff6,
1182
1183
  0x16fff,
1183
- 0x18cd5,
1184
+ 0x18cda,
1184
1185
  0x18cfe,
1185
- 0x18d1e,
1186
+ 0x18d20,
1186
1187
  0x18d7f,
1187
1188
  0x18df2,
1189
+ 0x18dff,
1190
+ 0x19191,
1191
+ 0x1919f,
1192
+ 0x191d2,
1188
1193
  0x1afef,
1189
1194
  0x1aff3,
1190
1195
  0x1aff4,
@@ -1192,7 +1197,7 @@ module Terminal
1192
1197
  0x1affc,
1193
1198
  0x1affe,
1194
1199
  0x1afff,
1195
- 0x1b122,
1200
+ 0x1b128,
1196
1201
  0x1b131,
1197
1202
  0x1b132,
1198
1203
  0x1b14f,
@@ -1200,7 +1205,7 @@ module Terminal
1200
1205
  0x1b154,
1201
1206
  0x1b155,
1202
1207
  0x1b163,
1203
- 0x1b167,
1208
+ 0x1b168,
1204
1209
  0x1b16f,
1205
1210
  0x1b2fb,
1206
1211
  0x1bc9c,
@@ -1211,6 +1216,8 @@ module Terminal
1211
1216
  0x1cf2d,
1212
1217
  0x1cf2f,
1213
1218
  0x1cf46,
1219
+ 0x1d126,
1220
+ 0x1d128,
1214
1221
  0x1d166,
1215
1222
  0x1d169,
1216
1223
  0x1d172,
@@ -1221,6 +1228,8 @@ module Terminal
1221
1228
  0x1d1ad,
1222
1229
  0x1d241,
1223
1230
  0x1d244,
1231
+ 0x1d25a,
1232
+ 0x1d25c,
1224
1233
  0x1d2ff,
1225
1234
  0x1d356,
1226
1235
  0x1d35f,
@@ -1287,6 +1296,8 @@ module Terminal
1287
1296
  0x1f190,
1288
1297
  0x1f19a,
1289
1298
  0x1f1ac,
1299
+ 0x1f1ad,
1300
+ 0x1f1ae,
1290
1301
  0x1f1ff,
1291
1302
  0x1f202,
1292
1303
  0x1f20f,
@@ -1342,13 +1353,15 @@ module Terminal
1342
1353
  0x1f6cf,
1343
1354
  0x1f6d2,
1344
1355
  0x1f6d4,
1345
- 0x1f6d8,
1356
+ 0x1f6d9,
1346
1357
  0x1f6db,
1347
1358
  0x1f6df,
1348
1359
  0x1f6ea,
1349
1360
  0x1f6ec,
1350
1361
  0x1f6f3,
1351
1362
  0x1f6fc,
1363
+ 0x1f7d9,
1364
+ 0x1f7da,
1352
1365
  0x1f7df,
1353
1366
  0x1f7eb,
1354
1367
  0x1f7ef,
@@ -1362,17 +1375,15 @@ module Terminal
1362
1375
  0x1fa6f,
1363
1376
  0x1fa7c,
1364
1377
  0x1fa7f,
1365
- 0x1fa8a,
1366
- 0x1fa8d,
1367
1378
  0x1fac6,
1368
1379
  0x1fac7,
1369
1380
  0x1fac8,
1370
- 0x1facc,
1371
- 0x1fadc,
1381
+ 0x1facb,
1382
+ 0x1fadd,
1372
1383
  0x1fade,
1373
- 0x1faea,
1384
+ 0x1faeb,
1374
1385
  0x1faee,
1375
- 0x1faf8,
1386
+ 0x1fafa,
1376
1387
  0x1ffff,
1377
1388
  0x2fffd,
1378
1389
  0x2ffff,
@@ -1876,8 +1887,6 @@ module Terminal
1876
1887
  0,
1877
1888
  1,
1878
1889
  0,
1879
- 1,
1880
- 0,
1881
1890
  nil,
1882
1891
  1,
1883
1892
  nil,
@@ -2549,6 +2558,10 @@ module Terminal
2549
2558
  1,
2550
2559
  0,
2551
2560
  1,
2561
+ 0,
2562
+ 1,
2563
+ 0,
2564
+ 1,
2552
2565
  2,
2553
2566
  0,
2554
2567
  1,
@@ -2578,6 +2591,14 @@ module Terminal
2578
2591
  1,
2579
2592
  2,
2580
2593
  1,
2594
+ 2,
2595
+ 1,
2596
+ 2,
2597
+ 1,
2598
+ 0,
2599
+ 1,
2600
+ 0,
2601
+ 1,
2581
2602
  0,
2582
2603
  1,
2583
2604
  0,
@@ -2689,6 +2710,8 @@ module Terminal
2689
2710
  2,
2690
2711
  1,
2691
2712
  2,
2713
+ 1,
2714
+ 2,
2692
2715
  0,
2693
2716
  2,
2694
2717
  1,
@@ -9,15 +9,15 @@ module Terminal
9
9
  # @see Terminal::Text
10
10
  #
11
11
  # @example Basic word-wrapping
12
- # fmt = Terminal::Text::Formatter.new('Hello World, this is a test')
13
- # fmt.lines(width: 12)
12
+ # formatter = Terminal::Text::Formatter.new('Hello World, this is a test')
13
+ # formatter.lines(width: 12)
14
14
  # # => ["Hello World,", "this is a", "test"]
15
15
  #
16
16
  # @example Formatted output with alignment
17
17
  # Terminal::Text::Formatter.format(
18
18
  # 'Hello', align: :center, width: 20
19
19
  # )
20
- # # => [" Hello "]
20
+ # # => ["       Hello        "]
21
21
  class Formatter
22
22
  # Parse text into lines, optionally with display widths.
23
23
  #
@@ -55,7 +55,7 @@ module Terminal
55
55
  #
56
56
  # @param (see #initialize)
57
57
  # @param (see #format)
58
- # @result (see #format)
58
+ # @return (see #format)
59
59
  # @raise (see #format)
60
60
  def self.format(
61
61
  *str,
@@ -80,6 +80,31 @@ module Terminal
80
80
  )
81
81
  end
82
82
 
83
+ # Format text with alignment.
84
+ #
85
+ # @see #fmt
86
+ #
87
+ # @example
88
+ # Terminal::Text::Formatter.fmt('Hi', align: :center, width: 10)
89
+ # # => ["    Hi    "]
90
+ #
91
+ # @param (see #initialize)
92
+ # @param (see #fmt)
93
+ # @return (see #fmt)
94
+ # @raise (see #fmt)
95
+ def self.fmt(
96
+ *str,
97
+ ansi: true,
98
+ bbcode: true,
99
+ spaces: true,
100
+ eol: true,
101
+ align: nil,
102
+ width: nil,
103
+ height: nil
104
+ )
105
+ new(*str, ansi:, bbcode:, spaces:, eol:).fmt(align:, width:, height:)
106
+ end
107
+
83
108
  # Create a new formatter with given text and options.
84
109
  #
85
110
  # @param str [Array<#to_s>] text to process
@@ -108,8 +133,8 @@ module Terminal
108
133
  # Word-wrap and return lines with their display widths.
109
134
  #
110
135
  # @example
111
- # fmt = Terminal::Text::Formatter.new('Hello World')
112
- # fmt.lines_with_size(width: 5)
136
+ # formatter = Terminal::Text::Formatter.new('Hello World')
137
+ # formatter.lines_with_size(width: 5)
113
138
  # # => [["Hello", 5], ["World", 5]]
114
139
  #
115
140
  # @param width [#to_i, nil] maximum line width in columns;
@@ -127,8 +152,8 @@ module Terminal
127
152
  # Word-wrap and return lines as strings.
128
153
  #
129
154
  # @example
130
- # fmt = Terminal::Text::Formatter.new('Hello World')
131
- # fmt.lines(width: 5)
155
+ # formatter = Terminal::Text::Formatter.new('Hello World')
156
+ # formatter.lines(width: 5)
132
157
  # # => ["Hello", "World"]
133
158
  #
134
159
  # @param width [#to_i, nil] maximum line width in columns
@@ -138,16 +163,22 @@ module Terminal
138
163
 
139
164
  # Format lines with alignment, padding, and decorations.
140
165
  #
141
- # @example Centered text with padding
142
- # fmt = Terminal::Text::Formatter.new('Hi')
143
- # fmt.format(align: :center, width: 10, padding: [0, 1])
144
- # # => [" Hi "]
166
+ # @see #fmt
167
+ #
168
+ # @example Prefix each line
169
+ # formatter = Terminal::Text::Formatter.new("Hello\nWorld")
170
+ # formatter.format(prefix: '> ')
171
+ # # => ["> Hello", "> World"]
172
+ # @example Framed box with padding
173
+ # formatter = Terminal::Text::Formatter.new('Hi')
174
+ # formatter.format(width: 6, padding: [1, 2], prefix: '|', suffix: '|')
175
+ # # => ["|      |", "|  Hi  |", "|      |"]
145
176
  #
146
177
  # @param align [Symbol, nil] text alignment:
147
178
  # `:left`, `:right`, `:center`, or `nil` (no fill)
148
179
  # @param width [#to_i, nil] output line width in columns
149
180
  # @param height [#to_i, nil] number of output lines;
150
- # negative values take lines from the end
181
+ # negative values take lines from the end; `0` returns `[""]`
151
182
  # @param padding [#to_i, Array, nil] CSS-style padding
152
183
  # - 1 value: all sides;
153
184
  # - 2 values: [vertical, horizontal];
@@ -165,6 +196,10 @@ module Terminal
165
196
  prefix: nil,
166
197
  suffix: nil
167
198
  )
199
+ if padding.nil? && prefix.nil? && suffix.nil?
200
+ return fmt(align:, width:, height:)
201
+ end
202
+
168
203
  top, right, bottom, left = _padding(padding)
169
204
 
170
205
  if height
@@ -200,11 +235,9 @@ module Terminal
200
235
  left = left < 1 ? prefix.to_s : "#{prefix}#{space[left]}"
201
236
  right = right < 1 ? suffix.to_s : "#{space[right]}#{suffix}"
202
237
 
203
- # TODO: styling
204
-
205
238
  case align
206
- when :left
207
- lws.each { |l, s| ret << "#{left}#{l}#{space[w - s]}#{right}" }
239
+ when nil, false
240
+ lws.each { |l, _| ret << "#{left}#{l}#{right}" }
208
241
  when :right
209
242
  lws.each { |l, s| ret << "#{left}#{space[w - s]}#{l}#{right}" }
210
243
  when :center
@@ -214,13 +247,58 @@ module Terminal
214
247
  ret << "#{left}#{space[s - rs]}#{l}#{space[rs]}#{right}"
215
248
  end
216
249
  else
217
- lws.each { |l, _| ret << "#{left}#{l}#{right}" }
250
+ lws.each { |l, s| ret << "#{left}#{l}#{space[w - s]}#{right}" }
218
251
  end
219
252
 
220
253
  return ret.fill(empty, ret.size, bottom) if bottom > 0
221
254
  ret.empty? ? ret << '' : ret
222
255
  end
223
256
 
257
+ # Format lines with alignment.
258
+ #
259
+ # Like {#format} without padding, prefix, and suffix.
260
+ #
261
+ # @see #format
262
+ #
263
+ # @example
264
+ # Terminal::Text::Formatter.new('Hi').fmt(align: :right, width: 6)
265
+ # # => ["    Hi"]
266
+ #
267
+ # @param align (see #format)
268
+ # @param width (see #format)
269
+ # @param height (see #format)
270
+ # @return (see #format)
271
+ # @raise (see #format)
272
+ def fmt(width: nil, height: nil, align: nil)
273
+ if height
274
+ return [] if (height = height.to_i) == 0
275
+ height = -height if (tail = (height < 0))
276
+ end
277
+
278
+ lines =
279
+ if width
280
+ w = (width = width.to_i)
281
+ raise(ArgumentError, "invalid width - #{width.inspect}") if w < 1
282
+ _limited(w)
283
+ else
284
+ @unlimited || _unlimited
285
+ end
286
+
287
+ return [+''] if lines.empty?
288
+
289
+ lines = tail ? lines.last(height) : lines.take(height) if height
290
+ w ||= lines.max_by(&:last)[-1] # unless width
291
+
292
+ return lines.map(&:first) unless align
293
+ space = SPACE_CACHE.dup
294
+ return lines.map { |l, s| "#{space[w - s]}#{l}" } if align == :right
295
+ return lines.map { |l, s| "#{l}#{space[w - s]}" } if align != :center
296
+ lines.map do |l, s|
297
+ rs = ((s = w - s) / 2.0).round
298
+ rs < 1 ? l : "#{space[s - rs]}#{l}#{space[rs]}"
299
+ end
300
+ end
301
+
224
302
  # Whether the formatter has any content.
225
303
  #
226
304
  # @return [true, false]
@@ -229,8 +307,8 @@ module Terminal
229
307
  # Maximum display width of any line.
230
308
  #
231
309
  # @example
232
- # fmt = Terminal::Text::Formatter.new("short\na longer line")
233
- # fmt.max_line_width # => 13
310
+ # formatter = Terminal::Text::Formatter.new("short\na longer line")
311
+ # formatter.max_line_width # => 13
234
312
  #
235
313
  # @return [Integer] widest line in columns, `0` when empty
236
314
  def max_line_width
@@ -352,9 +430,7 @@ module Terminal
352
430
  next if current.is_a?(Space) # ignore trailing space
353
431
 
354
432
  # handle Word...
355
- splitted = current.split(width)
356
- last = splitted.pop
357
-
433
+ *splitted, last = current.split(width)
358
434
  if csi.empty?
359
435
  splitted.each { ret << [it.to_str, it.size] }
360
436
  else
@@ -368,7 +444,10 @@ module Terminal
368
444
  size = last.size
369
445
  end
370
446
 
371
- ret.pop if ret[-1] == ['', 0]
447
+ if ret.size > 1
448
+ str, width = ret[-1]
449
+ ret.pop if width == 0 && str.empty?
450
+ end
372
451
  ret
373
452
  end
374
453
 
@@ -596,11 +675,10 @@ module Terminal
596
675
  @chunks.each_with_index do |chunk, idx|
597
676
  if chunk.bytesize > 1 && chunk.getbyte(0) < 0x80 &&
598
677
  chunk.ascii_only?
599
- chunk.each_char { chars << it and sizes << 1 }
600
- else
601
- chars << chunk
602
- sizes << @sizes[idx]
678
+ next chunk.each_char { chars << it and sizes << 1 }
603
679
  end
680
+ chars << chunk
681
+ sizes << @sizes[idx]
604
682
  end
605
683
  else
606
684
  chars = @chunks
@@ -676,7 +754,7 @@ module Terminal
676
754
  | \e\]\d+(?:;[^\a\e]+)*(?:\a|\e\\)
677
755
  | \s
678
756
  | \X
679
- /x
757
+ /x
680
758
 
681
759
  SPACE_BYTE =
682
760
  Hash
data/lib/terminal/text.rb CHANGED
@@ -42,7 +42,7 @@ module Terminal
42
42
  # @example
43
43
  # Terminal::Text.width('Hello') # => 5
44
44
  # Terminal::Text.width('[bold]Hi[/bold]') # => 2
45
- # Terminal::Text.width('[bold]Hi[/bold]', bbcode: false) # => 14
45
+ # Terminal::Text.width('[bold]Hi[/bold]', bbcode: false) # => 15
46
46
  #
47
47
  # @param str [#to_s] the string to measure
48
48
  # @param bbcode (see Formatter#initialize)
@@ -106,11 +106,30 @@ module Terminal
106
106
  #
107
107
  # @see Formatter.format
108
108
  #
109
+ # @example
110
+ # Terminal::Text.format(
111
+ # 'Hello World', width: 9, align: :left, prefix: '| ', suffix: ' |'
112
+ # )
113
+ # # => ["| Hello     |", "| World     |"]
114
+ #
109
115
  # @param (see Formatter.format)
110
116
  # @return (see Formatter.format)
111
117
  # @raise (see Formatter.format)
112
118
  def format(...) = Formatter.format(...)
113
119
 
120
+ # Format text with alignment.
121
+ #
122
+ # @see Formatter.fmt
123
+ #
124
+ # @example
125
+ # Terminal::Text.fmt('Hi', align: :center, width: 10)
126
+ # # => ["    Hi    "]
127
+ #
128
+ # @param (see Formatter.fmt)
129
+ # @return (see Formatter.fmt)
130
+ # @raise (see Formatter.fmt)
131
+ def fmt(...) = Formatter.fmt(...)
132
+
114
133
  # @deprecated Use {.lines} and iterate over the result.
115
134
  #
116
135
  # @comment @param str [Array<#to_s>] text to process
@@ -163,7 +182,7 @@ module Terminal
163
182
  )
164
183
  warn(
165
184
  '[DEPRECATED] `each_line_with_size` is deprecated ' \
166
- '– use `.lines_width_size.each` instead',
185
+ '– use `.lines_with_size.each` instead',
167
186
  category: :deprecated,
168
187
  uplevel: 1
169
188
  )
@@ -212,7 +231,6 @@ module Terminal
212
231
  def char_width(char)
213
232
  ord = char.ord
214
233
  return @ctrlchar_width[ord] if ord < 0x20
215
- # CharWidth knows this rule too - a comparison just beats a memo hit
216
234
  return ord < 0xa1 ? 1 : CharWidth[ord] if char.bytesize == 1
217
235
  return 2 if ord == 0x1f3f3 && @very_special_flags.include?(char)
218
236
  sum = 0
@@ -220,7 +238,7 @@ module Terminal
220
238
  char.each_codepoint do |cp|
221
239
  next skip = false if skip
222
240
  next skip = true if cp == 0x200d # zero width joiner
223
- sum += CharWidth[cp]
241
+ sum += cp < 0xa1 ? 1 : CharWidth[cp]
224
242
  end
225
243
  sum
226
244
  end
@@ -231,7 +249,6 @@ module Terminal
231
249
  "🏳\u{FE0F}\u{200D}⚧\u{FE0F}" # transgender flag
232
250
  ].freeze
233
251
 
234
- # indexed by ordinal, only ever asked for control characters
235
252
  @ctrlchar_width = [
236
253
  0,
237
254
  1,
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Terminal
4
4
  # The version number of the gem.
5
- VERSION = '1.0.8'
5
+ VERSION = '1.0.9'
6
6
  end
data/lib/terminal.rb CHANGED
@@ -72,28 +72,52 @@ module Terminal
72
72
 
73
73
  # Execute a shell command.
74
74
  #
75
- # @overload sh(*cmd, **options)
76
- # Run a command and capture output.
75
+ # Output lines are collected or yielded without their trailing newline.
76
+ #
77
+ # @overload sh(*cmd, env: {}, shell: false, input: nil, **options)
78
+ # Run a command and capture its output.
79
+ #
80
+ # @example
81
+ # status, output, error = Terminal.sh('echo', 'hello')
82
+ # output # => ["hello"]
83
+ #
77
84
  # @param cmd [Array<String>] command and arguments
78
- # @param options [Hash] options passed to the shell runner
85
+ # @param env [Hash] environment variables for the command
86
+ # @param shell [true, false]
87
+ # when true the `cmd` is executed via the shell,
88
+ # when false, the executable is direct executed
89
+ # (see `Process.spawn` for this)
90
+ # @param input [String, IO, Array<String>, Enumerable, nil]
91
+ # data written to the command's standard input
92
+ # @param options [Hash] passed to `Process.spawn`
93
+ # (`:in`, `:out`, `:err` are ignored)
79
94
  # @return [Array<Process::Status, Array<String>, Array<String>>]
80
95
  # `[status, stdout_lines, stderr_lines]`
96
+ # @return [nil] when the command can not be executed
97
+ #
98
+ # @overload sh(*cmd, env: {}, shell: false, input: nil, **options, &block)
99
+ # Run a command and stream its output to a block.
100
+ #
81
101
  # @example
82
- # status, output, error = Terminal.sh('echo', 'hello')
83
- # output # => ["hello\n"]
102
+ # Terminal.sh('make') do |line, type|
103
+ # Terminal.puts(type == :error ? "[red]#{line}[/]" : line)
104
+ # end
84
105
  #
85
- # @overload sh(*cmd, **options, &block)
86
- # Run a command and stream output to a block.
87
106
  # @param cmd [Array<String>] command and arguments
88
- # @param options [Hash] options passed to the shell runner
107
+ # @param env [Hash] environment variables for the command
108
+ # @param shell [true, false]
109
+ # when true the `cmd` is executed via the shell,
110
+ # when false, the executable is direct executed
111
+ # (see `Process.spawn` for this)
112
+ # @param input [String, IO, Array<String>, Enumerable, nil]
113
+ # data written to the command's standard input
114
+ # @param options [Hash] passed to `Process.spawn`
115
+ # (`:in`, `:out`, `:err` are ignored)
89
116
  # @yield [line, type] called for each output line
90
117
  # @yieldparam line [String] the output line
91
118
  # @yieldparam type [Symbol] `:output` or `:error`
92
119
  # @return [Process::Status] the exit status
93
- # @example
94
- # Terminal.sh('make') do |line, type|
95
- # Terminal.puts(type == :error ? "[red]#{line}[/]" : line)
96
- # end
120
+ # @return [nil] when the command can not be executed
97
121
  #
98
122
  # @raise [ArgumentError] if no command is given
99
123
  def sh(*cmd, **, &)
@@ -101,7 +125,8 @@ module Terminal
101
125
  return Shell.run(*cmd, **, &) if block_given?
102
126
  out = []
103
127
  err = []
104
- [Shell.run(*cmd, **) { |l, t| (t == :error ? err : out) << l }, out, err]
128
+ ret = Shell.run(*cmd, **) { |l, t| (t == :error ? err : out) << l }
129
+ [ret, out, err] if ret
105
130
  end
106
131
 
107
132
  #
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: 1.0.8
4
+ version: 1.0.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Blumtritt
@@ -60,7 +60,7 @@ metadata:
60
60
  yard.run: yard
61
61
  source_code_uri: https://codeberg.org/mblumtritt/Terminal.rb
62
62
  bug_tracker_uri: https://codeberg.org/mblumtritt/Terminal.rb/issues
63
- documentation_uri: https://rubydoc.info/gems/terminal_rb/1.0.8
63
+ documentation_uri: https://rubydoc.info/gems/terminal_rb/1.0.9
64
64
  rdoc_options: []
65
65
  require_paths:
66
66
  - lib
@@ -75,7 +75,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
75
75
  - !ruby/object:Gem::Version
76
76
  version: '0'
77
77
  requirements: []
78
- rubygems_version: 4.0.18
78
+ rubygems_version: 4.0.21
79
79
  specification_version: 4
80
80
  summary: Fast terminal access with ANSI, CSIu, mouse events, BBCode, word-wise line
81
81
  break support and much more.