terminal_rb 0.12.2 → 0.14.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.
@@ -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,8 +24,8 @@ 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
31
  str = bbcode ? Ansi.unbbcode(str) : str.to_s
@@ -45,15 +45,15 @@ module Terminal
45
45
  # Terminal::Text.each_line('This is a simple test 😀', limit: 6).to_a
46
46
  # # => ["This", "is a", "simple", "test", "😀"]
47
47
  #
48
- # @param [#to_s, ...] text
48
+ # @param text [#to_s, ...]
49
49
  # text objects to process
50
- # @param [#to_i, nil] limit
50
+ # @param limit [#to_i, nil]
51
51
  # optionally limit line size
52
- # @param [true, false] bbcode
52
+ # @param bbcode [true, false]
53
53
  # whether to interprete embedded BBCode (see {Ansi.bbcode})
54
- # @param [true, false] ansi
54
+ # @param ansi [true, false]
55
55
  # whether to keep embedded ANSI control codes
56
- # @param [true, false] ignore_newline
56
+ # @param ignore_newline [true, false]
57
57
  # wheter to ignore embedded line breaks (`"\r\n"` or `"\n"`)
58
58
  # @yield [String] text line
59
59
  # @return [Enumerator] when no block given
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Terminal
4
4
  # The version number of the gem.
5
- VERSION = '0.12.2'
5
+ VERSION = '0.14.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,14 @@ 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?
178
154
  self
179
155
  end
180
156
 
181
157
  # Writes the given object to the terminal.
182
158
  # Interprets embedded BBCode.
183
159
  #
184
- # @param [#to_s] object object to write
160
+ # @param object [#to_s] object to write
185
161
  # @return [Terminal] itself
186
162
  def <<(object)
187
163
  @out.write(Ansi.bbcode(object)) if @out && object != nil
@@ -194,8 +170,8 @@ module Terminal
194
170
  # Writes the given objects to the terminal.
195
171
  # Optionally interprets embedded BBCode.
196
172
  #
197
- # @param [Array<#to_s>] objects any number of objects to write
198
- # @param [true|false] bbcode whether to interpret embedded BBCode
173
+ # @param objects [Array<#to_s>] any number of objects to write
174
+ # @param bbcode [true|false] whether to interpret embedded BBCode
199
175
  # @return [nil]
200
176
  def print(*objects, bbcode: true)
201
177
  return unless @out
@@ -225,14 +201,78 @@ module Terminal
225
201
  @out = nil
226
202
  end
227
203
 
228
- private
204
+ # Execute a command and report command output line by line
205
+ # or capture the output.
206
+ #
207
+ # @example Execute a command wih arguments
208
+ # ret, out, = Terminal.sh('curl', '--version')
209
+ # raise('command not found - curl') unless ret
210
+ # puts out
211
+ #
212
+ # @example Execute shell commands and print error
213
+ # ret, _, err = Terminal.sh("mkdir '/test' && cd '/test'")
214
+ # raise('unable to execute') unless ret
215
+ # puts("error #{ret.exitstatus}: #{err.join}") unless ret.success?
216
+ #
217
+ # @example Copy a file content to clipboard
218
+ # command = ENV.fetch('CLIP_COPY', 'pbcopy')
219
+ # File.open(__FILE__) do |file|
220
+ # ret, = Terminal.sh(*command, input: file)
221
+ # raise("command not found - #{command}") unless ret
222
+ # end
223
+ #
224
+ # @overload sh(*cmd, **options)
225
+ # @return [[Process::Status, output, error]]
226
+ # the status of the process successfully executed,
227
+ # the captured output,
228
+ # the captured error output,
229
+ # @return [nil]
230
+ # when the command was not executable
231
+ #
232
+ # @overload sh(*cmd, **options)
233
+ # @yieldparam line [String]
234
+ # next received line from the process
235
+ # @yieldparam type [:output, :error]
236
+ # whether the received line is standard output or error
237
+ # @return [Process::Status]
238
+ # the status of the process successfully executed
239
+ # @return [nil]
240
+ # when the command was not executable
241
+ #
242
+ # @param cmd [Array<#to_s>]
243
+ # command line or command with arguments
244
+ # @param options [Hash]
245
+ # options how to execute the new process;
246
+ # see `Process.spawn` for default execution options provided by Ruby
247
+ # @option options [Hash<String,String>] :env
248
+ # key/value pairs added to the ENV of the process;
249
+ # with option `:unsetenv_others` set to `true` it replaces the ENV
250
+ # @option options [true, false] :shell
251
+ # whether to create a seperate shell or not
252
+ # @option options [#readpartial, #to_io, Array, #each, #to_a, #to_s] :input
253
+ # piped to the command as input
254
+ def sh(*cmd, **options, &block)
255
+ return Shell.exec(cmd, **options, &block) if block_given?
256
+ out = []
257
+ err = []
258
+ [
259
+ Shell.exec(cmd, **options) do |line, type|
260
+ (type == :error ? err : out) << line
261
+ end,
262
+ out,
263
+ err
264
+ ]
265
+ end
229
266
 
230
- def _write(str)
267
+ # @private
268
+ def raw_write(str)
231
269
  @out&.write(str)
232
270
  rescue IOError
233
271
  @out = nil
234
272
  end
235
273
 
274
+ private
275
+
236
276
  def _default_size
237
277
  rows = ENV['LINES'].to_i
238
278
  columns = ENV['COLUMNS'].to_i
@@ -300,5 +340,6 @@ module Terminal
300
340
  dir = "#{__dir__}/terminal"
301
341
  autoload :Text, "#{dir}/text.rb"
302
342
  autoload :Detect, "#{dir}/detect.rb"
303
- private_constant :Detect
343
+ autoload :Shell, "#{dir}/shell.rb"
344
+ private_constant :Detect, :Shell
304
345
  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.12.2
4
+ version: 0.14.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: []
@@ -29,12 +30,12 @@ files:
29
30
  - examples/key-codes.rb
30
31
  - lib/terminal.rb
31
32
  - lib/terminal/ansi.rb
32
- - lib/terminal/ansi/attributes.rb
33
33
  - lib/terminal/ansi/named_colors.rb
34
34
  - lib/terminal/detect.rb
35
35
  - lib/terminal/input.rb
36
36
  - lib/terminal/input/key_event.rb
37
37
  - lib/terminal/rspec/helper.rb
38
+ - lib/terminal/shell.rb
38
39
  - lib/terminal/text.rb
39
40
  - lib/terminal/text/char_width.rb
40
41
  - lib/terminal/version.rb
@@ -53,7 +54,7 @@ require_paths:
53
54
  - lib
54
55
  required_ruby_version: !ruby/object:Gem::Requirement
55
56
  requirements:
56
- - - ">="
57
+ - - ">"
57
58
  - !ruby/object:Gem::Version
58
59
  version: '3.0'
59
60
  required_rubygems_version: !ruby/object:Gem::Requirement
@@ -62,7 +63,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
62
63
  - !ruby/object:Gem::Version
63
64
  version: '0'
64
65
  requirements: []
65
- rubygems_version: 3.7.1
66
+ rubygems_version: 3.6.9
66
67
  specification_version: 4
67
68
  summary: Fast terminal access with ANSI, CSIu, BBCode, word-wise line break support.
68
69
  test_files: []