terminal_rb 0.13.0 → 0.15.0

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: 756d1e0e016472a063f9d64650f4731ef6e8fe272d6a84c4e001119f050af3ea
4
- data.tar.gz: 74c24ba64e08e6dc79605c17b0ded59ecb8784a45f95dbe5be7f231c889fdab7
3
+ metadata.gz: 23d41a32cd285a8ec05dde44ac26131e207f567b4137ff4c1b144171abb72dd6
4
+ data.tar.gz: 28325ab102a8ecd1ec6e9f2b9196aa85f8bf8d59898b6105a26d6afa2cc1a065
5
5
  SHA512:
6
- metadata.gz: 68bb81c425cb5df44650acc351a28cbbb3eccaea53b405167f1207ba3dc528093de6322bac087fad9301da7470711b4cd399f120233637e9b92f1727e9e2d4c9
7
- data.tar.gz: 9e855ec0401d22c615fd6ff25e2adae7717ecbf3578ea40425e5ef580b9520ee23130c8d49814034f682590166d2319070c17c18407962fc8782b18c1fffe4fa
6
+ metadata.gz: ff88211e6ac2b869db965ef8861ddf312699c4cef971deea0f2b75bfac0f77df8f23936aa2a6870adfd60167391a720e76a089918b1e8b2bb35842d6fd32cb0a
7
+ data.tar.gz: '091504b7dfebe143f8df736f52db891a08a27d4bac9763706af06def5a9c2e413b620300c4188bd700c529b2446cb1ecb351a4425772bdd051b46332b96ce4bd'
data/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # Terminal.rb ![version](https://img.shields.io/gem/v/terminal_rb?label=)
2
2
 
3
- Terminal access with super fast ANSI control codes support and [BBCode-like](https://en.wikipedia.org/wiki/BBCode) embedded text attribute syntax.
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, and correct special key recognition 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/Terminal)
7
+ - Help: [rubydoc.info](https://rubydoc.info/gems/terminal_rb/index)
8
8
 
9
9
  ## Features
10
10
 
data/examples/info.rb CHANGED
@@ -12,5 +12,6 @@ Terminal.puts <<~TEXT
12
12
  Terminal Size: [b]#{Terminal.size.join(' x ')}[/]
13
13
  Cursor Position: [b]#{Terminal.pos&.join(', ')}[/]
14
14
  Input Mode: [b]#{Terminal.input_mode}[/]
15
+ Link Support: [b]#{Terminal::Ansi.link('https://codeberg.org/mblumtritt/Terminal.rb', 'Link')}[/]
15
16
 
16
17
  TEXT
data/examples/text.rb ADDED
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../lib/terminal'
4
+
5
+ LOREM = <<~IPSUM
6
+ This example outputs [i bright_white]Lorem ipsum[/] text so that it takes up 75% of the screen width and fits the height of your screen.
7
+
8
+ [i bright_white]Lorem ipsum[/] dolor sit amet, [green]consetetur[/fg] sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore [b]magna[/b] aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata [yellow]sanctus[/fg] est [i bright_white]Lorem ipsum[/] dolor sit amet. [i bright_white]Lorem ipsum[/] dolor sit amet, [green]consetetur[/fg] sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore [b]magna[/b] aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata [yellow]sanctus[/fg] est [i bright_white]Lorem ipsum[/] dolor sit amet. [i bright_white]Lorem ipsum[/] dolor sit amet, [green]consetetur[/fg] sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore [b]magna[/b] aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata [yellow]sanctus[/fg] est [i bright_white]Lorem ipsum[/] dolor sit amet.
9
+
10
+ Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla [red]facilisis[/fg] at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. [i bright_white]Lorem ipsum[/] dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore [b]magna[/b] aliquam erat volutpat.
11
+
12
+ Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla [red]facilisis[/fg] at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.
13
+
14
+ Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. [i bright_white]Lorem ipsum[/] dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore [b]magna[/b] aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.
15
+
16
+ Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla [red]facilisis[/fg].
17
+
18
+ At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata [yellow]sanctus[/fg] est [i bright_white]Lorem ipsum[/] dolor sit amet. [i bright_white]Lorem ipsum[/] dolor sit amet, [green]consetetur[/fg] sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore [b]magna[/b] aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata [yellow]sanctus[/fg] est [i bright_white]Lorem ipsum[/] dolor sit amet. [i bright_white]Lorem ipsum[/] dolor sit amet, [green]consetetur[/fg] sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd [b]magna[/b] no rebum. [yellow]sanctus[/fg] sea sed takimata ut vero voluptua. est [i bright_white]Lorem ipsum[/] dolor sit amet. [i bright_white]Lorem ipsum[/] dolor sit amet, [green]consetetur[/fg] sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore [b]magna[/b] aliquyam erat.
19
+
20
+ Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore [b]magna[/b] aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata [yellow]sanctus[/fg] est [i bright_white]Lorem ipsum[/] dolor sit amet. [i bright_white]Lorem ipsum[/] dolor sit amet, [green]consetetur[/fg] sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore [b]magna[/b] aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata [yellow]sanctus[/fg] est [i bright_white]Lorem ipsum[/] dolor sit amet. [i bright_white]Lorem ipsum[/] dolor sit amet, [green]consetetur[/fg] sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore [b]magna[/b] aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata [yellow]sanctus[/fg].
21
+
22
+ [i bright_white]Lorem ipsum[/] dolor sit amet, [green]consetetur[/fg] sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore [b]magna[/b] aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata [yellow]sanctus[/fg] est [i bright_white]Lorem ipsum[/] dolor sit amet. [i bright_white]Lorem ipsum[/] dolor sit amet, [green]consetetur[/fg] sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore [b]magna[/b] aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata [yellow]sanctus[/fg] est [i bright_white]Lorem ipsum[/] dolor sit amet. [i bright_white]Lorem ipsum[/] dolor sit amet, [green]consetetur[/fg] sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore [b]magna[/b] aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata [yellow]sanctus[/fg] est [i bright_white]Lorem ipsum[/] dolor sit amet.
23
+
24
+ Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla [red]facilisis[/fg] at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. [i bright_white]Lorem ipsum[/] dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore [b]magna[/b] aliquam erat volutpat.
25
+
26
+ Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla [red]facilisis[/fg] at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.
27
+
28
+ Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. [i bright_white]Lorem ipsum[/] dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore [b]magna[/b] aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.
29
+ IPSUM
30
+
31
+ rows, cols = Terminal.size
32
+ puts Terminal::Text.each(LOREM, limit: cols * 0.75).take(rows - 1)
data/lib/terminal/ansi.rb CHANGED
@@ -387,7 +387,7 @@ module Terminal
387
387
 
388
388
  # Erase screen part.
389
389
  #
390
- # @param [:below, :above, :all, :scrollback] part screen part to erase
390
+ # @param part [:below, :above, :all, :scrollback] screen part to erase
391
391
  # @return (see cursor_up)
392
392
  def screen_erase(part = :all)
393
393
  "\e[#{
@@ -444,7 +444,7 @@ module Terminal
444
444
 
445
445
  # Erase part of line.
446
446
  #
447
- # @param [:to_end, :to_start, :all] part line part to erase
447
+ # @param part [:to_end, :to_start, :all] line part to erase
448
448
  # @return (see cursor_up)
449
449
  def line_erase(part = :all)
450
450
  "\e[#{
@@ -468,7 +468,7 @@ module Terminal
468
468
  # Tabby,
469
469
  # WezTerm.
470
470
  #
471
- # @param [#to_s] title text
471
+ # @param title [#to_s] text
472
472
  # @return (see cursor_up)
473
473
  def title(title) = "\e]0;#{title}\a"
474
474
 
@@ -481,11 +481,22 @@ module Terminal
481
481
  # Tabby,
482
482
  # WezTerm.
483
483
  #
484
- # @param [#to_s] url URL to link to
485
- # @param [#to_s] text text to display for the link
484
+ # @param url [#to_s] URL to link to
485
+ # @param text [#to_s] text to display for the link
486
486
  # @return (see cursor_up)
487
487
  def link(url, text) = "\e]8;;#{url}\a#{text}\e]8;;\a"
488
488
 
489
+ # Show a simple notification.
490
+ # This is not widely supported; works for
491
+ # Ghosty,
492
+ # iTerm2,
493
+ # Kitty,
494
+ # WezTerm.
495
+ #
496
+ # @param text [#to_s] text to display
497
+ # @return (see cursor_up)
498
+ def notify(text) = "\e]9;#{text}\a"
499
+
489
500
  # Create scaled text.
490
501
  # It uses the
491
502
  # [text sizing protocol](https://sw.kovidgoyal.net/kitty/text-sizing-protocol).
@@ -497,18 +508,19 @@ module Terminal
497
508
  # @example Half-height Greeting
498
509
  # Terminal::Ansi.scale('Hello Ruby!', fracn: 1, fracd: 2, vertical: :centered)
499
510
  #
500
- # @param [#to_s] text text to scale
501
- # @param [Integer, nil] scale
511
+ # @param text [#to_s]
512
+ # text to scale
513
+ # @param scale [Integer, nil]
502
514
  # overall scale size, range 1..7
503
- # @param [Integer, nil] width
515
+ # @param width [Integer, nil]
504
516
  # with in cells, range 0..7
505
- # @param [Integer, nil] fracn
517
+ # @param fracn [Integer, nil]
506
518
  # numerator for the fractional scale, range: 0..15
507
- # @param [Integer, nil] fracd
519
+ # @param fracd [Integer, nil]
508
520
  # denominator for the fractional scale, range: 0..15, > fracn
509
- # @param [:top, :bottom, :centered, nil] vertical
521
+ # @param vertical [:top, :bottom, :centered, nil]
510
522
  # vertical alignment to use for fractionally scaled text
511
- # @param [:left, :right, :centered, nil] horizontal
523
+ # @param horizontal [:left, :right, :centered, nil]
512
524
  # horizontal alignment to use for fractionally scaled text
513
525
  # @return (see cursor_up)
514
526
  def scale(
@@ -587,7 +599,7 @@ module Terminal
587
599
 
588
600
  @re_test =
589
601
  /
590
- (?:\e\[[\d;:\?]*[ABCDEFGHJKSTfminsuhl])
602
+ (?:\e\[[\d;:?]*[ABCDEFGHJKSTfminsuhl])
591
603
  |
592
604
  (?:\e\]\d+(?:;[^\a\e]+)*(?:\a|\e\\))
593
605
  /x
@@ -787,66 +799,66 @@ module Terminal
787
799
  autoload :NAMED_COLORS, "#{__dir__}/ansi/named_colors.rb"
788
800
  private_constant :NAMED_COLORS
789
801
 
790
- # @!visibility private
802
+ # @private
791
803
  RESET = self[:reset].freeze
792
804
 
793
- # @!visibility private
805
+ # @private
794
806
  FULL_RESET = "\ec"
795
807
 
796
- # @!visibility private
808
+ # @private
797
809
  CURSOR_HOME = cursor_pos(nil, nil).freeze
798
- # @!visibility private
810
+ # @private
799
811
  CURSOR_FIRST_ROW = cursor_pos(1).freeze
800
- # @!visibility private
812
+ # @private
801
813
  CURSOR_FIRST_COLUMN = cursor_column(1).freeze
802
814
 
803
- # @!visibility private
815
+ # @private
804
816
  CURSOR_SHOW = "\e[?25h"
805
- # @!visibility private
817
+ # @private
806
818
  CURSOR_HIDE = "\e[?25l"
807
819
 
808
820
  # @comment CURSOR_POS_SAVE_SCO = "\e[s"
809
821
  # @comment CURSOR_POS_SAVE_DEC = "\e7"
810
- # @!visibility private
822
+ # @private
811
823
  CURSOR_POS_SAVE = "\e7"
812
824
 
813
825
  # @comment CURSOR_POS_RESTORE_SCO = "\e[u"
814
826
  # @comment CURSOR_POS_RESTORE_DEC = "\e8"
815
- # @!visibility private
827
+ # @private
816
828
  CURSOR_POS_RESTORE = "\e8"
817
829
 
818
- # @!visibility private
830
+ # @private
819
831
  SCREEN_ERASE = screen_erase.freeze
820
- # @!visibility private
832
+ # @private
821
833
  SCREEN_ERASE_BELOW = screen_erase(:below).freeze
822
- # @!visibility private
834
+ # @private
823
835
  SCREEN_ERASE_ABOVE = screen_erase(:above).freeze
824
- # @!visibility private
836
+ # @private
825
837
  SCREEN_ERASE_SCROLLBACK = screen_erase(:scrollback).freeze
826
838
 
827
- # @!visibility private
839
+ # @private
828
840
  SCREEN_SAVE = "\e[?47h"
829
- # @!visibility private
841
+ # @private
830
842
  SCREEN_RESTORE = "\e[?47l"
831
843
 
832
- # @!visibility private
844
+ # @private
833
845
  # @comment at least Kitty requires CURSOR_HOME too
834
846
  SCREEN_ALTERNATE = "\e[?1049h#{CURSOR_HOME}".freeze
835
- # @!visibility private
847
+ # @private
836
848
  SCREEN_ALTERNATE_OFF = "\e[?1049l"
837
849
 
838
- # @!visibility private
850
+ # @private
839
851
  SCREEN_REVERSE_MODE_ON = "\e[?5h"
840
- # @!visibility private
852
+ # @private
841
853
  SCREEN_REVERSE_MODE_OFF = "\e[?5l"
842
854
 
843
- # @!visibility private
855
+ # @private
844
856
  LINE_ERASE = line_erase.freeze
845
- # @!visibility private
857
+ # @private
846
858
  LINE_ERASE_TO_END = line_erase(:to_end).freeze
847
- # @!visibility private
859
+ # @private
848
860
  LINE_ERASE_TO_START = line_erase(:to_start).freeze
849
- # @!visibility private
861
+ # @private
850
862
  LINE_ERASE_PREV = "#{cursor_prev_line(nil)}#{LINE_ERASE}".freeze
851
863
 
852
864
  # @comment seems not widely supported:
@@ -7,7 +7,7 @@ module Terminal
7
7
  class KeyEvent
8
8
  class << self
9
9
  # @attribute [w] caching
10
- # @return [true, false] whether KeyCodes should be cached
10
+ # @return [true, false] whether {KeyEvent}s should be cached
11
11
  def caching = !!@cache
12
12
 
13
13
  # @attribute [w] caching
@@ -21,7 +21,7 @@ module Terminal
21
21
 
22
22
  # Translate a keyboard input string into a related KeyEvent
23
23
  #
24
- # @param [String] raw keyboard input
24
+ # @param raw [String] keyboard input
25
25
  # @return [KeyEvent] related key event
26
26
  def [](raw)
27
27
  return new(raw, *@single_key[raw.ord]) if raw.size == 1
@@ -52,7 +52,7 @@ module Terminal
52
52
  unknown(raw)
53
53
  end
54
54
 
55
- # @!visibility private
55
+ # @private
56
56
  def new(raw, key = raw, modifier = 0, extra = nil)
57
57
  @cache ? (@cache[raw] ||= super.freeze) : super.freeze
58
58
  end
@@ -164,7 +164,7 @@ module Terminal
164
164
  attr_reader :modifier
165
165
 
166
166
  # @comment for mouse events
167
- # @!visibility private
167
+ # @private
168
168
  attr_reader :extra
169
169
 
170
170
  # Name of the key event.
@@ -188,16 +188,16 @@ module Terminal
188
188
  # @return [Array<Symbol, String>] all pressed keys
189
189
  def to_a = @ary.dup
190
190
 
191
- # @!visibility private
191
+ # @private
192
192
  def to_ary = simple? ? [@raw] : [@raw, @name]
193
193
 
194
- # @!visibility private
194
+ # @private
195
195
  def to_s = @name.dup
196
196
 
197
- # @!visibility private
197
+ # @private
198
198
  def inspect = "<#{self.class.name} #{to_ary.map(&:inspect).join(' ')}>"
199
199
 
200
- # @!visibility private
200
+ # @private
201
201
  def freeze
202
202
  @raw.freeze
203
203
  @key.freeze
@@ -28,7 +28,7 @@ module Terminal
28
28
  #
29
29
  # @deprecated Use rather {read_key_event} for better input handling.
30
30
  #
31
- # @param [:named, :raw, :both] mode modifies the result
31
+ # @param mode [:named, :raw, :both] modifies the result
32
32
  # @return [String] key code ("as is") in `:raw` mode
33
33
  # @return [String] key name in `:named` mode
34
34
  # @return [[String, String]] key code and key name in `:both` mode
@@ -64,8 +64,8 @@ module Terminal
64
64
  return :dumb if im == 'dumb'
65
65
  return :legacy if im == 'legacy'
66
66
  return :dumb unless STDIN.tty?
67
- if ansi? && _write("\e[>1u\e[?u\e[c") && csi_u?
68
- at_exit { _write("\e[<u") }
67
+ if ansi? && raw_write("\e[>1u\e[?u\e[c") && csi_u?
68
+ at_exit { raw_write("\e[<u") }
69
69
  return :csi_u
70
70
  end
71
71
  :legacy
@@ -99,10 +99,10 @@ module Terminal
99
99
  # urxvt: '1015'
100
100
  # pixel: '1016'
101
101
  opts = each_move ? '1000;1003;1006;1015' : '1000;1006;1015'
102
- opts = _write("\e[?#{opts}h") ? "\e[?#{opts}l" : nil
102
+ opts = raw_write("\e[?#{opts}h") ? "\e[?#{opts}l" : nil
103
103
  read_tty
104
104
  ensure
105
- _write(opts) if opts
105
+ raw_write(opts) if opts
106
106
  end
107
107
 
108
108
  def read_tty
@@ -53,7 +53,7 @@ RSpec.shared_context 'with Terminal.rb' do |ansi: true, application: :kitty, col
53
53
  nil
54
54
  end
55
55
 
56
- allow(Terminal).to receive(:_write) do |object|
56
+ allow(Terminal).to receive(:raw_write) do |object|
57
57
  stdout.push(object = object.to_s)
58
58
  object.bytesize
59
59
  end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Terminal
4
+ module Shell
5
+ class << self
6
+ def exec(cmd, env: {}, shell: false, input: nil, **options)
7
+ options = options.except(:in, :out, :err)
8
+ if shell
9
+ cmd = cmd.map! { _escape(_1) }.join(' ')
10
+ elsif cmd.size == 1 && cmd[0].include?(' ')
11
+ cmd = cmd[0]
12
+ end
13
+
14
+ input = Input.for(input)
15
+ ret = nil
16
+ with_io(options, input) do |cio, out_r, err_r, in_w|
17
+ thread = Process.detach(Process.spawn(env, *cmd, options))
18
+ begin
19
+ cio.each(&:close)
20
+ read = [out_r, err_r]
21
+ write = [in_w] if in_w
22
+ while !read.empty? || write
23
+ rr, wr, = IO.select(read, write)
24
+ if rr.include?(out_r)
25
+ begin
26
+ yield(out_r.readline(chomp: true), :output)
27
+ rescue SystemCallError, IOError
28
+ read.delete(out_r)
29
+ end
30
+ end
31
+ if rr.include?(err_r)
32
+ begin
33
+ yield(err_r.readline(chomp: true), :error)
34
+ rescue SystemCallError, IOError
35
+ read.delete(err_r)
36
+ end
37
+ end
38
+ next if wr.empty?
39
+ begin
40
+ next if input.call(in_w)
41
+ in_w.close
42
+ write = nil
43
+ rescue SystemCallError, IOError
44
+ write = nil
45
+ end
46
+ end
47
+ ensure
48
+ ret = thread.join.value
49
+ end
50
+ end
51
+ ret
52
+ rescue SystemCallError, IOError
53
+ nil
54
+ end
55
+
56
+ private
57
+
58
+ def with_io(options, input)
59
+ IO.pipe do |out_r, out_w|
60
+ IO.pipe do |err_r, err_w|
61
+ cio = [options[:out] = out_w, options[:err] = err_w]
62
+ return yield(cio, out_r, err_r) unless input
63
+ IO.pipe do |in_r, in_w|
64
+ cio << (options[:in] = in_r)
65
+ in_w.sync = true
66
+ yield(cio, out_r, err_r, in_w)
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ def _escape(str)
73
+ return +"''" if str.empty?
74
+ str = str.dup
75
+ str.gsub!(%r{[^A-Za-z0-9_\-.,:+/@\n]}, "\\\\\\&")
76
+ str.gsub!("\n", "'\n'")
77
+ str
78
+ end
79
+ end
80
+
81
+ module Input
82
+ def self.for(obj)
83
+ return unless obj
84
+ return CopyWriter.new(obj) if obj.respond_to?(:readpartial)
85
+ return CopyWriter.new(obj.to_io) if obj.respond_to?(:to_io)
86
+ return ArrayWriter.new(obj) if obj.is_a?(Array)
87
+ return EnumerableWriter.new(obj) if obj.respond_to?(:each)
88
+ return ArrayWriter.new(obj.to_a) if obj.respond_to?(:to_a)
89
+ Writer.new(obj)
90
+ end
91
+
92
+ class Writer
93
+ def call(io) = (io.write(_next) if @obj)
94
+ def initialize(obj) = (@obj = obj)
95
+ def _next = (_, @obj = @obj, nil).first
96
+ end
97
+
98
+ class CopyWriter < Writer
99
+ def call(io) = (IO.copy_stream(_next, io) if @obj)
100
+ end
101
+
102
+ class ArrayWriter
103
+ def call(io) = io.write(@ry[@idx += 1] || return)
104
+
105
+ def initialize(ary)
106
+ @ry = ary
107
+ @idx = -1
108
+ end
109
+ end
110
+
111
+ class EnumerableWriter
112
+ def call(io)
113
+ io.write(@enum.next)
114
+ rescue StopIteration
115
+ false
116
+ end
117
+
118
+ def initialize(enum)
119
+ @enum =
120
+ if enum.respond_to?(:enum_for)
121
+ enum.enum_for(:each)
122
+ else
123
+ Enumerator.new { |y| enum.each { y << _1 } }
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ private_constant :Shell
131
+ end
@@ -3,7 +3,7 @@
3
3
  module Terminal
4
4
  module Text
5
5
  #
6
- # Generated file; based on Unicode v16.0.0
6
+ # Generated file; based on Unicode v17.0.0
7
7
  #
8
8
  module CharWidth
9
9
  def self.[](ord) = @width[@last.bsearch_index { ord <= _1 }]
@@ -415,7 +415,9 @@ module Terminal
415
415
  0x1a7e,
416
416
  0x1a7f,
417
417
  0x1aaf,
418
- 0x1ace,
418
+ 0x1add,
419
+ 0x1adf,
420
+ 0x1aeb,
419
421
  0x1aff,
420
422
  0x1b03,
421
423
  0x1b33,
@@ -887,7 +889,7 @@ module Terminal
887
889
  0x10d6d,
888
890
  0x10eaa,
889
891
  0x10eac,
890
- 0x10efb,
892
+ 0x10ef9,
891
893
  0x10eff,
892
894
  0x10f45,
893
895
  0x10f50,
@@ -1037,6 +1039,12 @@ module Terminal
1037
1039
  0x11a96,
1038
1040
  0x11a97,
1039
1041
  0x11a99,
1042
+ 0x11b5f,
1043
+ 0x11b60,
1044
+ 0x11b61,
1045
+ 0x11b64,
1046
+ 0x11b65,
1047
+ 0x11b66,
1040
1048
  0x11c2f,
1041
1049
  0x11c36,
1042
1050
  0x11c37,
@@ -1099,13 +1107,13 @@ module Terminal
1099
1107
  0x16fe3,
1100
1108
  0x16fe4,
1101
1109
  0x16fef,
1102
- 0x16ff1,
1110
+ 0x16ff6,
1103
1111
  0x16fff,
1104
- 0x187f7,
1105
- 0x187ff,
1106
1112
  0x18cd5,
1107
1113
  0x18cfe,
1108
- 0x18d08,
1114
+ 0x18d1e,
1115
+ 0x18d7f,
1116
+ 0x18df2,
1109
1117
  0x1afef,
1110
1118
  0x1aff3,
1111
1119
  0x1aff4,
@@ -1178,6 +1186,14 @@ module Terminal
1178
1186
  0x1e4ef,
1179
1187
  0x1e5ed,
1180
1188
  0x1e5ef,
1189
+ 0x1e6e2,
1190
+ 0x1e6e3,
1191
+ 0x1e6e5,
1192
+ 0x1e6e6,
1193
+ 0x1e6ed,
1194
+ 0x1e6ef,
1195
+ 0x1e6f4,
1196
+ 0x1e6f5,
1181
1197
  0x1e8cf,
1182
1198
  0x1e8d6,
1183
1199
  0x1e943,
@@ -1251,7 +1267,7 @@ module Terminal
1251
1267
  0x1f6cf,
1252
1268
  0x1f6d2,
1253
1269
  0x1f6d4,
1254
- 0x1f6d7,
1270
+ 0x1f6d8,
1255
1271
  0x1f6db,
1256
1272
  0x1f6df,
1257
1273
  0x1f6ea,
@@ -1271,14 +1287,16 @@ module Terminal
1271
1287
  0x1fa6f,
1272
1288
  0x1fa7c,
1273
1289
  0x1fa7f,
1274
- 0x1fa89,
1275
- 0x1fa8e,
1290
+ 0x1fa8a,
1291
+ 0x1fa8d,
1276
1292
  0x1fac6,
1277
- 0x1facd,
1293
+ 0x1fac7,
1294
+ 0x1fac8,
1295
+ 0x1facc,
1278
1296
  0x1fadc,
1279
1297
  0x1fade,
1280
- 0x1fae9,
1281
- 0x1faef,
1298
+ 0x1faea,
1299
+ 0x1faee,
1282
1300
  0x1faf8,
1283
1301
  0x1ffff,
1284
1302
  0x2fffd,
@@ -1748,6 +1766,8 @@ module Terminal
1748
1766
  1,
1749
1767
  0,
1750
1768
  1,
1769
+ 0,
1770
+ 1,
1751
1771
  -1,
1752
1772
  1,
1753
1773
  -1,
@@ -2381,6 +2401,12 @@ module Terminal
2381
2401
  1,
2382
2402
  0,
2383
2403
  1,
2404
+ 0,
2405
+ 1,
2406
+ 0,
2407
+ 1,
2408
+ 0,
2409
+ 1,
2384
2410
  2,
2385
2411
  0,
2386
2412
  1,
@@ -2468,6 +2494,14 @@ module Terminal
2468
2494
  1,
2469
2495
  0,
2470
2496
  1,
2497
+ 0,
2498
+ 1,
2499
+ 0,
2500
+ 1,
2501
+ 0,
2502
+ 1,
2503
+ 0,
2504
+ 1,
2471
2505
  2,
2472
2506
  1,
2473
2507
  2,
@@ -2570,6 +2604,8 @@ module Terminal
2570
2604
  1,
2571
2605
  2,
2572
2606
  1,
2607
+ 2,
2608
+ 1,
2573
2609
  0,
2574
2610
  1,
2575
2611
  -1,
data/lib/terminal/text.rb CHANGED
@@ -24,12 +24,11 @@ module Terminal
24
24
  # some are ambiguous. The function uses {ambiguous_char_width} for each of
25
25
  # these characters.
26
26
  #
27
- # @param [#to_s] str object to process
28
- # @param [true|false] bbcode whether to interpret embedded BBCode
27
+ # @param object [#to_s] str to process
28
+ # @param bbcode [true|false] whether to interpret embedded BBCode
29
29
  # @return [Integer] display width
30
30
  def width(str, bbcode: true)
31
- str = bbcode ? Ansi.unbbcode(str) : str.to_s
32
- return 0 if str.empty?
31
+ return 0 if (str = bbcode ? Ansi.unbbcode(str) : str.to_s).empty?
33
32
  str = str.encode(@encoding) if str.encoding != @encoding
34
33
  width = 0
35
34
  str.scan(WIDTH_SCANNER) do |sp, gc|
@@ -45,15 +44,15 @@ module Terminal
45
44
  # Terminal::Text.each_line('This is a simple test 😀', limit: 6).to_a
46
45
  # # => ["This", "is a", "simple", "test", "😀"]
47
46
  #
48
- # @param [#to_s, ...] text
47
+ # @param text [#to_s, ...]
49
48
  # text objects to process
50
- # @param [#to_i, nil] limit
49
+ # @param limit [#to_i, nil]
51
50
  # optionally limit line size
52
- # @param [true, false] bbcode
51
+ # @param bbcode [true, false]
53
52
  # whether to interprete embedded BBCode (see {Ansi.bbcode})
54
- # @param [true, false] ansi
53
+ # @param ansi [true, false]
55
54
  # whether to keep embedded ANSI control codes
56
- # @param [true, false] ignore_newline
55
+ # @param ignore_newline [true, false]
57
56
  # wheter to ignore embedded line breaks (`"\r\n"` or `"\n"`)
58
57
  # @yield [String] text line
59
58
  # @return [Enumerator] when no block given
@@ -68,14 +67,14 @@ module Terminal
68
67
  &block
69
68
  )
70
69
  unless limit
71
- snippeds = as_snippeds(text, bbcode, ansi, ignore_newline, Word)
72
- return block ? lines(snippeds, &block) : to_enum(:lines, snippeds)
70
+ snippets = as_snippets(text, bbcode, ansi, ignore_newline, Word)
71
+ return block ? lines(snippets, &block) : to_enum(:lines, snippets)
73
72
  end
74
73
  limit = limit.to_i
75
74
  raise(ArgumentError, "invalid limit - #{limit}") if limit < 1
76
- snippeds = as_snippeds(text, bbcode, ansi, ignore_newline, WordEx)
77
- return lim_lines(snippeds, limit, &block) if block
78
- to_enum(:lim_lines, snippeds, limit)
75
+ snippets = as_snippets(text, bbcode, ansi, ignore_newline, WordEx)
76
+ return lim_lines(snippets, limit, &block) if block
77
+ to_enum(:lim_lines, snippets, limit)
79
78
  end
80
79
  alias each each_line
81
80
 
@@ -98,14 +97,14 @@ module Terminal
98
97
  &block
99
98
  )
100
99
  unless limit
101
- snippeds = as_snippeds(text, bbcode, ansi, ignore_newline, Word)
102
- return block ? pairs(snippeds, &block) : to_enum(:pairs, snippeds)
100
+ snippets = as_snippets(text, bbcode, ansi, ignore_newline, Word)
101
+ return block ? pairs(snippets, &block) : to_enum(:pairs, snippets)
103
102
  end
104
103
  limit = limit.to_i
105
104
  raise(ArgumentError, "invalid limit - #{limit}") if limit < 1
106
- snippeds = as_snippeds(text, bbcode, ansi, ignore_newline, WordEx)
107
- return lim_pairs(snippeds, limit, &block) if block
108
- to_enum(:lim_pairs, snippeds, limit)
105
+ snippets = as_snippets(text, bbcode, ansi, ignore_newline, WordEx)
106
+ return lim_pairs(snippets, limit, &block) if block
107
+ to_enum(:lim_pairs, snippets, limit)
109
108
  end
110
109
  alias each_with_size each_line_with_size
111
110
 
@@ -123,12 +122,12 @@ module Terminal
123
122
  char.size < 2 ? 1 : char.each_char.sum { char_width(_1) }
124
123
  end
125
124
 
126
- def lim_pairs(snippeds, limit)
125
+ def lim_pairs(snippets, limit)
127
126
  line = @empty.dup
128
127
  size = 0
129
128
  csi = nil
130
- snippeds.each do |snipped|
131
- if snipped == :space
129
+ snippets.each do |snippet|
130
+ if snippet == :space
132
131
  next if size == 0
133
132
  next line << ' ' if (size += 1) <= limit
134
133
  yield(line, size - 1)
@@ -136,31 +135,31 @@ module Terminal
136
135
  next size = 0
137
136
  end
138
137
 
139
- if snipped == :nl
138
+ if snippet == :nl
140
139
  line[-1] == ' ' ? yield(line.chop, size - 1) : yield(line, size)
141
140
  line = "#{csi}"
142
141
  next size = 0
143
142
  end
144
143
 
145
- if snipped == :hard_nl
144
+ if snippet == :hard_nl
146
145
  line[-1] == ' ' ? yield(line.chop, size - 1) : yield(line, size)
147
146
  line = @empty.dup
148
147
  csi = nil
149
148
  next size = 0
150
149
  end
151
150
 
152
- if snipped == CsiEnd
151
+ if snippet == CsiEnd
153
152
  line << CsiEnd if csi
154
153
  next csi = nil
155
154
  end
156
155
 
157
- next line << (csi = snipped) if snipped.is_a?(Csi)
158
- next line << snipped if snipped.is_a?(Osc)
156
+ next line << (csi = snippet) if snippet.is_a?(Csi)
157
+ next line << snippet if snippet.is_a?(Osc)
159
158
 
160
159
  # Word:
161
160
 
162
- if (ns = size + snipped.size) <= limit
163
- line << snipped
161
+ if (ns = size + snippet.size) <= limit
162
+ line << snippet
164
163
  next size = ns
165
164
  end
166
165
 
@@ -170,16 +169,16 @@ module Terminal
170
169
  end
171
170
  yield(line, size) if size != 0
172
171
 
173
- if snipped.size <= limit
174
- line = "#{csi}#{snipped}"
175
- next size = snipped.size
172
+ if snippet.size <= limit
173
+ line = "#{csi}#{snippet}"
174
+ next size = snippet.size
176
175
  end
177
176
 
178
- words = snipped.split(limit)
177
+ words = snippet.split(limit)
179
178
  if words[-1].size <= limit
180
- snipped = words.pop
181
- line = "#{csi}#{snipped}"
182
- size = snipped.size
179
+ snippet = words.pop
180
+ line = "#{csi}#{snippet}"
181
+ size = snippet.size
183
182
  else
184
183
  line = "#{csi}"
185
184
  size = 0
@@ -189,51 +188,51 @@ module Terminal
189
188
  nil
190
189
  end
191
190
 
192
- def pairs(snippeds)
191
+ def pairs(snippets)
193
192
  line = @empty.dup
194
193
  size = 0
195
194
  csi = nil
196
- snippeds.each do |snipped|
197
- if snipped == :space
195
+ snippets.each do |snippet|
196
+ if snippet == :space
198
197
  next if size == 0
199
198
  line << ' '
200
199
  next size += 1
201
200
  end
202
201
 
203
- if snipped == :nl
202
+ if snippet == :nl
204
203
  line[-1] == ' ' ? yield(line.chop, size - 1) : yield(line, size)
205
204
  line = "#{csi}"
206
205
  next size = 0
207
206
  end
208
207
 
209
- if snipped == :hard_nl
208
+ if snippet == :hard_nl
210
209
  line[-1] == ' ' ? yield(line.chop, size - 1) : yield(line, size)
211
210
  line = @empty.dup
212
211
  csi = nil
213
212
  next size = 0
214
213
  end
215
214
 
216
- if snipped == CsiEnd
215
+ if snippet == CsiEnd
217
216
  line << CsiEnd if csi
218
217
  next csi = nil
219
218
  end
220
219
 
221
- next line << (csi = snipped) if snipped.is_a?(Csi)
222
- next line << snipped if snipped.is_a?(Osc)
220
+ next line << (csi = snippet) if snippet.is_a?(Csi)
221
+ next line << snippet if snippet.is_a?(Osc)
223
222
 
224
223
  # Word:
225
- line << snipped
226
- size += snipped.size
224
+ line << snippet
225
+ size += snippet.size
227
226
  end
228
227
  nil
229
228
  end
230
229
 
231
- def lim_lines(snippeds, limit)
230
+ def lim_lines(snippets, limit)
232
231
  line = @empty.dup
233
232
  size = 0
234
233
  csi = nil
235
- snippeds.each do |snipped|
236
- if snipped == :space
234
+ snippets.each do |snippet|
235
+ if snippet == :space
237
236
  next if size == 0
238
237
  next line << ' ' if (size += 1) <= limit
239
238
  yield(line)
@@ -241,31 +240,31 @@ module Terminal
241
240
  next size = 0
242
241
  end
243
242
 
244
- if snipped == :nl
243
+ if snippet == :nl
245
244
  yield(line[-1] == ' ' ? line.chop : line)
246
245
  line = "#{csi}"
247
246
  next size = 0
248
247
  end
249
248
 
250
- if snipped == :hard_nl
249
+ if snippet == :hard_nl
251
250
  yield(line[-1] == ' ' ? line.chop : line)
252
251
  line = @empty.dup
253
252
  csi = nil
254
253
  next size = 0
255
254
  end
256
255
 
257
- if snipped == CsiEnd
256
+ if snippet == CsiEnd
258
257
  line << CsiEnd if csi
259
258
  next csi = nil
260
259
  end
261
260
 
262
- next line << (csi = snipped) if snipped.is_a?(Csi)
263
- next line << snipped if snipped.is_a?(Osc)
261
+ next line << (csi = snippet) if snippet.is_a?(Csi)
262
+ next line << snippet if snippet.is_a?(Osc)
264
263
 
265
264
  # Word:
266
265
 
267
- if (ns = size + snipped.size) <= limit
268
- line << snipped
266
+ if (ns = size + snippet.size) <= limit
267
+ line << snippet
269
268
  next size = ns
270
269
  end
271
270
 
@@ -275,16 +274,16 @@ module Terminal
275
274
  end
276
275
  yield(line) if size != 0
277
276
 
278
- if snipped.size <= limit
279
- line = "#{csi}#{snipped}"
280
- next size = snipped.size
277
+ if snippet.size <= limit
278
+ line = "#{csi}#{snippet}"
279
+ next size = snippet.size
281
280
  end
282
281
 
283
- words = snipped.split(limit)
282
+ words = snippet.split(limit)
284
283
  if words[-1].size <= limit
285
- snipped = words.pop
286
- line = "#{csi}#{snipped}"
287
- size = snipped.size
284
+ snippet = words.pop
285
+ line = "#{csi}#{snippet}"
286
+ size = snippet.size
288
287
  else
289
288
  line = "#{csi}"
290
289
  size = 0
@@ -294,46 +293,46 @@ module Terminal
294
293
  nil
295
294
  end
296
295
 
297
- def lines(snippeds)
296
+ def lines(snippets)
298
297
  line = @empty.dup
299
298
  size = 0
300
299
  csi = nil
301
- snippeds.each do |snipped|
302
- if snipped == :space
300
+ snippets.each do |snippet|
301
+ if snippet == :space
303
302
  next if size == 0
304
303
  line << ' '
305
304
  next size += 1
306
305
  end
307
306
 
308
- if snipped == :nl
307
+ if snippet == :nl
309
308
  yield(line[-1] == ' ' ? line.chop : line)
310
309
  line = "#{csi}"
311
310
  next size = 0
312
311
  end
313
312
 
314
- if snipped == :hard_nl
313
+ if snippet == :hard_nl
315
314
  yield(line[-1] == ' ' ? line.chop : line)
316
315
  line = @empty.dup
317
316
  csi = nil
318
317
  next size = 0
319
318
  end
320
319
 
321
- if snipped == CsiEnd
320
+ if snippet == CsiEnd
322
321
  line << CsiEnd if csi
323
322
  next csi = nil
324
323
  end
325
324
 
326
- next line << (csi = snipped) if snipped.is_a?(Csi)
327
- next line << snipped if snipped.is_a?(Osc)
325
+ next line << (csi = snippet) if snippet.is_a?(Csi)
326
+ next line << snippet if snippet.is_a?(Osc)
328
327
 
329
328
  # Word:
330
- line << snipped
331
- size += snipped.size
329
+ line << snippet
330
+ size += snippet.size
332
331
  end
333
332
  nil
334
333
  end
335
334
 
336
- def as_snippeds(text, bbcode, ansi, ignore_newline, word_class)
335
+ def as_snippets(text, bbcode, ansi, ignore_newline, word_class)
337
336
  ret = []
338
337
  last = nil
339
338
  text.each do |txt|
@@ -451,7 +450,7 @@ module Terminal
451
450
  SCAN_EXPR =
452
451
  /\G(?:
453
452
  (\r?\n)
454
- | (\e\[[\d;:\?]*[ABCDEFGHJKSTfhilmnsu])
453
+ | (\e\[[\d;:?]*[ABCDEFGHJKSTfhilmnsu])
455
454
  | (\e\]\d+(?:;[^\a\e]+)*(?:\a|\e\\))
456
455
  | (\s+)
457
456
  | (\X)
@@ -459,7 +458,7 @@ module Terminal
459
458
 
460
459
  WIDTH_SCANNER =
461
460
  /\G(?:
462
- (?:\e\[[\d;:\?]*[ABCDEFGHJKSTfhilmnsu])
461
+ (?:\e\[[\d;:?]*[ABCDEFGHJKSTfhilmnsu])
463
462
  | (?:\e\]\d+(?:;[^\a\e]+)*(?:\a|\e\\))
464
463
  | (\s+)
465
464
  | (\X)
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Terminal
4
4
  # The version number of the gem.
5
- VERSION = '0.13.0'
5
+ VERSION = '0.15.0'
6
6
  end
data/lib/terminal.rb CHANGED
@@ -7,20 +7,20 @@ require_relative 'terminal/input'
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
9
  # syntax (see {Ansi.bbcode}).
10
- #
11
10
  # It automagically detects whether your terminal supports ANSI features, like
12
11
  # coloring (see {colors}) or the
13
12
  # [CSIu protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol) support
14
13
  # (see {read_key_event} and {input_mode}).
15
14
  # It calculates the display width for Unicode chars (see {Text.width}) and help
16
- # you to display text with line formatting (see {Text.each_line}).
15
+ # you to display text with word-wise line breaks (see {Text.each_line}).
17
16
  #
18
17
  module Terminal
19
18
  class << self
20
19
  # Return true if the current terminal supports ANSI control codes.
21
- # When the terminal does not support it, {colors} will return `2` and all
22
- # output methods ({<<}, {print}, {puts}) will not forward ANSI control
23
- # codes to the terminal, {read_key_event} will not support CSIu.
20
+ # When the terminal does not support it, {colors} will return 2 (two) and
21
+ # all output methods ({<<}, {print}, {puts}) will not forward ANSI control
22
+ # codes to the terminal, {read_key_event} will not support extended key
23
+ # codes.
24
24
  #
25
25
  # @attribute [r] ansi?
26
26
  # @return [Boolean] whether ANSI control codes are supported
@@ -31,34 +31,10 @@ module Terminal
31
31
  # The detection supports a wide range of terminal emulators but may return
32
32
  # `nil` if an unsupported terminal was found. These are the supported
33
33
  # application IDs:
34
- #
35
- # - `:alacritty`,
36
- # - `:amiga`,
37
- # - `:code_edit`,
38
- # - `:dg_unix`,
39
- # - `:fluent`,
40
- # - `:ghostty`,
41
- # - `:hpterm`,
42
- # - `:hyper`,
43
- # - `:iterm`,
44
- # - `:macos`,
45
- # - `:mintty`,
46
- # - `:ms_terminal`,
47
- # - `:ncr260`,
48
- # - `:nsterm`,
49
- # - `:rio`,
50
- # - `:tabby`,
51
- # - `:terminator`,
52
- # - `:terminology`,
53
- # - `:terminus`,
54
- # - `:termite`,
55
- # - `:tmux`,
56
- # - `:vscode`,
57
- # - `:vt100`,
58
- # - `:warp`,
59
- # - `:wezterm`,
60
- # - `:wyse`,
61
- # - `:xnuppc`
34
+ # `:alacritty`, `:amiga`, `:code_edit`, `:dg_unix`, `:docker`, `:fluent`,
35
+ # `:hpterm`, `:hyper`, `:iterm`, `:macos`, `:mintty`, `:ms_terminal`,
36
+ # `:ncr260`, `:nsterm`, `:terminator`, `:terminology`, `:termite`,
37
+ # `:vt100`, `:warp`, `:wezterm`, `:wyse`, `:xnuppc`
62
38
  #
63
39
  # @attribute [r] application
64
40
  # @return [Symbol, nil] the application identifier
@@ -162,7 +138,7 @@ module Terminal
162
138
  #
163
139
  # @return [Terminal] itself
164
140
  def hide_cursor
165
- _write(Ansi::CURSOR_HIDE) if ansi? && (@cc += 1) == 1
141
+ raw_write(Ansi::CURSOR_HIDE) if ansi? && (@cc += 1) == 1
166
142
  self
167
143
  end
168
144
 
@@ -174,14 +150,38 @@ module Terminal
174
150
  #
175
151
  # @return [Terminal] itself
176
152
  def show_cursor
177
- _write(Ansi::CURSOR_SHOW) if @cc > 0 && (@cc -= 1).zero?
153
+ raw_write(Ansi::CURSOR_SHOW) if @cc > 0 && (@cc -= 1).zero?
154
+ self
155
+ end
156
+
157
+ # Show the alternate screen.
158
+ # Will not send the control code if the alternate screen is already used.
159
+ #
160
+ # When you called {show_alt_screen} n-times you need to call
161
+ # {hide_alt_screen} n-times to show the default screen again.
162
+ #
163
+ # @return [Terminal] itself
164
+ def show_alt_screen
165
+ raw_write(Ansi::SCREEN_ALTERNATE) if ansi? && (@as += 1) == 1
166
+ self
167
+ end
168
+
169
+ # Hide the alternate screen.
170
+ # Will not send the control code if the alternate screen is not used.
171
+ #
172
+ # When you called {show_alt_screen} n-times you need to call
173
+ # {hide_alt_screen} n-times to show the default screen again.
174
+ #
175
+ # @return [Terminal] itself
176
+ def hide_alt_screen
177
+ raw_write(Ansi::SCREEN_ALTERNATE_OFF) if @as > 0 && (@as -= 1).zero?
178
178
  self
179
179
  end
180
180
 
181
181
  # Writes the given object to the terminal.
182
182
  # Interprets embedded BBCode.
183
183
  #
184
- # @param [#to_s] object object to write
184
+ # @param object [#to_s] object to write
185
185
  # @return [Terminal] itself
186
186
  def <<(object)
187
187
  @out.write(Ansi.bbcode(object)) if @out && object != nil
@@ -194,8 +194,8 @@ module Terminal
194
194
  # Writes the given objects to the terminal.
195
195
  # Optionally interprets embedded BBCode.
196
196
  #
197
- # @param [Array<#to_s>] objects any number of objects to write
198
- # @param [true|false] bbcode whether to interpret embedded BBCode
197
+ # @param objects [Array<#to_s>] any number of objects to write
198
+ # @param bbcode [true|false] whether to interpret embedded BBCode
199
199
  # @return [nil]
200
200
  def print(*objects, bbcode: true)
201
201
  return unless @out
@@ -225,14 +225,78 @@ module Terminal
225
225
  @out = nil
226
226
  end
227
227
 
228
- private
228
+ # Execute a command and report command output line by line
229
+ # or capture the output.
230
+ #
231
+ # @example Execute a command wih arguments
232
+ # ret, out, = Terminal.sh('curl', '--version')
233
+ # raise('command not found - curl') unless ret
234
+ # puts out
235
+ #
236
+ # @example Execute shell commands and print error
237
+ # ret, _, err = Terminal.sh("mkdir '/test' && cd '/test'")
238
+ # raise('unable to execute') unless ret
239
+ # puts("error #{ret.exitstatus}: #{err.join}") unless ret.success?
240
+ #
241
+ # @example Copy a file content to clipboard
242
+ # command = ENV.fetch('CLIP_COPY', 'pbcopy')
243
+ # File.open(__FILE__) do |file|
244
+ # ret, = Terminal.sh(*command, input: file)
245
+ # raise("command not found - #{command}") unless ret
246
+ # end
247
+ #
248
+ # @overload sh(*cmd, **options)
249
+ # @return [[Process::Status, output, error]]
250
+ # the status of the process successfully executed,
251
+ # the captured output,
252
+ # the captured error output,
253
+ # @return [nil]
254
+ # when the command was not executable
255
+ #
256
+ # @overload sh(*cmd, **options)
257
+ # @yieldparam line [String]
258
+ # next received line from the process
259
+ # @yieldparam type [:output, :error]
260
+ # whether the received line is standard output or error
261
+ # @return [Process::Status]
262
+ # the status of the process successfully executed
263
+ # @return [nil]
264
+ # when the command was not executable
265
+ #
266
+ # @param cmd [Array<#to_s>]
267
+ # command line or command with arguments
268
+ # @param options [Hash]
269
+ # options how to execute the new process;
270
+ # see `Process.spawn` for default execution options provided by Ruby
271
+ # @option options [Hash<String,String>] :env
272
+ # key/value pairs added to the ENV of the process;
273
+ # with option `:unsetenv_others` set to `true` it replaces the ENV
274
+ # @option options [true, false] :shell
275
+ # whether to create a seperate shell or not
276
+ # @option options [#readpartial, #to_io, Array, #each, #to_a, #to_s] :input
277
+ # piped to the command as input
278
+ def sh(*cmd, **options, &block)
279
+ return Shell.exec(cmd, **options, &block) if block_given?
280
+ out = []
281
+ err = []
282
+ [
283
+ Shell.exec(cmd, **options) do |line, type|
284
+ (type == :error ? err : out) << line
285
+ end,
286
+ out,
287
+ err
288
+ ]
289
+ end
229
290
 
230
- def _write(str)
291
+ # @private
292
+ def raw_write(str)
231
293
  @out&.write(str)
232
294
  rescue IOError
233
295
  @out = nil
234
296
  end
235
297
 
298
+ private
299
+
236
300
  def _default_size
237
301
  rows = ENV['LINES'].to_i
238
302
  columns = ENV['COLUMNS'].to_i
@@ -250,7 +314,7 @@ module Terminal
250
314
  end
251
315
  end
252
316
 
253
- @cc = 0
317
+ @cc = @as = 0
254
318
  tty, @ansi = _determine_modes
255
319
 
256
320
  unless tty.nil?
@@ -300,5 +364,6 @@ module Terminal
300
364
  dir = "#{__dir__}/terminal"
301
365
  autoload :Text, "#{dir}/text.rb"
302
366
  autoload :Detect, "#{dir}/detect.rb"
303
- private_constant :Detect
367
+ autoload :Shell, "#{dir}/shell.rb"
368
+ private_constant :Detect, :Shell
304
369
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/terminal/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'terminal_rb'
7
+ spec.version = Terminal::VERSION
8
+ spec.summary =
9
+ 'Fast terminal access with ANSI, CSIu, BBCode, word-wise line break support.'
10
+ spec.description = <<~DESCRIPTION.tr("\n", ' ')
11
+ Terminal.rb supports you with input and output on your terminal.
12
+ Simple BBCode-like markup for attributes and coloring, word-wise line
13
+ breaks, and correct special key recognition enable you to implement your
14
+ CLI app quickly and easily.
15
+ DESCRIPTION
16
+
17
+ spec.author = 'Mike Blumtritt'
18
+ spec.licenses = %w[MIT Ruby]
19
+ spec.homepage = 'https://codeberg.org/mblumtritt/Terminal.rb'
20
+ spec.metadata['source_code_uri'] = spec.homepage
21
+ spec.metadata['bug_tracker_uri'] = "#{spec.homepage}/issues"
22
+ spec.metadata['documentation_uri'] = 'https://rubydoc.info/gems/terminal_rb'
23
+ spec.metadata['rubygems_mfa_required'] = 'true'
24
+ spec.metadata['yard.run'] = 'yard'
25
+
26
+ spec.required_ruby_version = '> 3.0'
27
+
28
+ spec.files = Dir['lib/**/*.rb']
29
+ spec.files += Dir['examples/*.rb']
30
+ spec.files += %w[terminal_rb.gemspec .yardopts]
31
+ spec.executables = %w[bbcode]
32
+ spec.extra_rdoc_files = %w[README.md]
33
+ end
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.13.0
4
+ version: 0.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Blumtritt
@@ -9,8 +9,9 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
- description: 'Terminal access with super fast ANSI control codes support, modern CSIu
13
- input, word-wise line break, BBCode-like embedded text attribute syntax. '
12
+ description: 'Terminal.rb supports you with input and output on your terminal. Simple
13
+ BBCode-like markup for attributes and coloring, word-wise line breaks, and correct
14
+ special key recognition enable you to implement your CLI app quickly and easily. '
14
15
  executables:
15
16
  - bbcode
16
17
  extensions: []
@@ -27,6 +28,7 @@ files:
27
28
  - examples/bbcode.rb
28
29
  - examples/info.rb
29
30
  - examples/key-codes.rb
31
+ - examples/text.rb
30
32
  - lib/terminal.rb
31
33
  - lib/terminal/ansi.rb
32
34
  - lib/terminal/ansi/named_colors.rb
@@ -34,13 +36,16 @@ files:
34
36
  - lib/terminal/input.rb
35
37
  - lib/terminal/input/key_event.rb
36
38
  - lib/terminal/rspec/helper.rb
39
+ - lib/terminal/shell.rb
37
40
  - lib/terminal/text.rb
38
41
  - lib/terminal/text/char_width.rb
39
42
  - lib/terminal/version.rb
40
43
  - lib/terminal_rb.rb
44
+ - terminal_rb.gemspec
41
45
  homepage: https://codeberg.org/mblumtritt/Terminal.rb
42
46
  licenses:
43
47
  - MIT
48
+ - Ruby
44
49
  metadata:
45
50
  source_code_uri: https://codeberg.org/mblumtritt/Terminal.rb
46
51
  bug_tracker_uri: https://codeberg.org/mblumtritt/Terminal.rb/issues
@@ -52,7 +57,7 @@ require_paths:
52
57
  - lib
53
58
  required_ruby_version: !ruby/object:Gem::Requirement
54
59
  requirements:
55
- - - ">="
60
+ - - ">"
56
61
  - !ruby/object:Gem::Version
57
62
  version: '3.0'
58
63
  required_rubygems_version: !ruby/object:Gem::Requirement
@@ -61,7 +66,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
61
66
  - !ruby/object:Gem::Version
62
67
  version: '0'
63
68
  requirements: []
64
- rubygems_version: 3.7.1
69
+ rubygems_version: 3.7.2
65
70
  specification_version: 4
66
71
  summary: Fast terminal access with ANSI, CSIu, BBCode, word-wise line break support.
67
72
  test_files: []