plywood 1.1.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9cb35cf97e0f00639e9c3ec01c009100eef846ae77c245daeb0f80f124f3d59d
4
+ data.tar.gz: eb0c36061977ab643b328daed9b33b569ee78df08c30594f4a93c9bccf024e55
5
+ SHA512:
6
+ metadata.gz: 3b5c5fb7d6f88f80ca2d46ffa2566eda56adf76e20128e87dfe02f4a2c1bcff34e88260b3f9c12377205638265031456dc6846a9c1ed552668f5dab608c2d45e
7
+ data.tar.gz: de6f5e1b9ac405eaf3322ed9c35ce1b9e5d3aaeb04d06a811cf9ac9e32a60eb6b2e1135e6ce4a38bf94318909bc3e3d4ea16e9acf80d8b290ad128a470789e4d
data/README.md ADDED
@@ -0,0 +1,47 @@
1
+ ![logo](logo.jpeg)
2
+
3
+ # Plywood
4
+
5
+ A simple way to get multiplexing; run commands concurrently, combine their output streams.
6
+
7
+ # Usage
8
+
9
+ Typically you feed it an array of items; Plywood yields the items one by one and expects you to make a command using that thing.
10
+
11
+ It then runs all the commands at the same time.
12
+
13
+ ```ruby
14
+ require "plywood"
15
+
16
+ items = %w[foo bar baz]
17
+
18
+ # There is no need to do this concurrently but this is just an example
19
+ Plywood::Commands.map(items) do |item|
20
+ "touch #{item}.txt" # the command that should be run
21
+ end
22
+ ```
23
+
24
+ Plywood, by default, will use `item.to_s` to use as the "process identifier". This identifier must be unique; and you are responsible for that. Often this is not a problem at all, say when you loop over a bunch of hostnames or chemical elements, but when you loop over a group of people it's very well possible that two of them share the same name.
25
+
26
+ To help with that you can specify the `id` parameter. If it's a `Symbol` Plywood will assume it's the name of a method on the item with arity 0 and call it. The return value is used as the process identifier. If it's a `Proc`; Plywood will call it with the item as the only argument.
27
+
28
+ ```ruby
29
+ my_favorite_things = [
30
+ Thing.new("raindrops on roses"),
31
+ Thing.new("whiskers on kittens"),
32
+ Thing.new("bright copper kettles"),
33
+ Thing.new("warm woolen mittens"),
34
+ Thing.new("brown paper packages tied up with strings")]
35
+
36
+ Plywood::Commands.map(my_favorite_things, id: :object_id) do |thing|
37
+ "touch #{thing.underscore}.txt"
38
+ end
39
+ ```
40
+
41
+ You can disable concurrent execution by using `sequential: true`:
42
+
43
+ ```ruby
44
+ Plywood::Commands.map(my_favorite_things, sequential: true) do |thing|
45
+ "touch #{thing.underscore}.txt"
46
+ end
47
+ ```
@@ -0,0 +1,38 @@
1
+ module Plywood
2
+ module Color
3
+ ANSI = {
4
+ reset: 0,
5
+ black: 30,
6
+ red: 31,
7
+ green: 32,
8
+ yellow: 33,
9
+ blue: 34,
10
+ magenta: 35,
11
+ cyan: 36,
12
+ white: 37,
13
+ bright_black: 30,
14
+ bright_red: 31,
15
+ bright_green: 32,
16
+ bright_yellow: 33,
17
+ bright_blue: 34,
18
+ bright_magenta: 35,
19
+ bright_cyan: 36,
20
+ bright_white: 37 }.freeze
21
+
22
+ def self.enable(io)
23
+ io.extend(self)
24
+ end
25
+
26
+ def color?
27
+ return false unless respond_to?(:isatty)
28
+ isatty && ENV["TERM"]
29
+ end
30
+
31
+ def color(name)
32
+ return "" unless color?
33
+ ansi = ANSI[name.to_sym]
34
+ return "" if ansi.nil?
35
+ "\e[#{ansi}m"
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,36 @@
1
+ require_relative "named_io"
2
+
3
+ module Plywood
4
+ class Command
5
+ attr_reader :command, :name
6
+ attr_reader :err, :out, :pid
7
+
8
+ def initialize(name, command)
9
+ @name = name.to_s
10
+ @command = command.to_s
11
+ @err, @err_writer = build_pipe(:err)
12
+ @out, @out_writer = build_pipe(:out)
13
+ end
14
+
15
+ def to_s
16
+ "#{name} `#{@command}`"
17
+ end
18
+
19
+ def ios
20
+ [err, out]
21
+ end
22
+
23
+ def run
24
+ @pid = ::Process.spawn(@command, err: @err_writer, out: @out_writer)
25
+ @err_writer.puts "started with pid #{@pid}"
26
+ self
27
+ end
28
+
29
+ private
30
+
31
+ def build_pipe(stream)
32
+ reader, writer = IO.pipe
33
+ [NamedIO.new(reader, @name, stream), writer]
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,32 @@
1
+ require "forwardable"
2
+ require_relative "command"
3
+
4
+ module Plywood
5
+ # A small report to show after the command has run.
6
+ class CommandReport
7
+ extend Forwardable
8
+
9
+ def_delegators :@command, :command, :name, :pid
10
+ def_delegators :@status, :success?, :to_i
11
+
12
+ def initialize(command, status)
13
+ raise ArgumentError, "command is nil" if command.nil?
14
+ raise ArgumentError, "status is nil" if status.nil?
15
+ @command = command
16
+ @status = status
17
+ end
18
+
19
+ def self.gather(status, commands)
20
+ command = commands.find { |item| item.pid == status.pid }
21
+ new(command, status)
22
+ end
23
+
24
+ def to_s
25
+ if success?
26
+ "#{name} was successful."
27
+ else
28
+ "#{name} failed with exit status #{to_i} (`#{command}`)."
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,108 @@
1
+ require_relative "command"
2
+ require_relative "command_report"
3
+ require_relative "logger"
4
+
5
+ module Plywood
6
+ class Commands
7
+ def initialize(io_handler_class: Logger, sequential: false)
8
+ @list = {}
9
+ @io_handler_class = io_handler_class
10
+ @sequential = sequential
11
+ yield self
12
+ run
13
+ end
14
+
15
+ def self.map(array, io_handler_class: Logger, id: :to_s, sequential: false)
16
+ new(io_handler_class: io_handler_class, sequential: sequential) do |ply|
17
+ array.each do |item|
18
+ command = yield item
19
+ name = name_for_item(item, id: id)
20
+ ply.register(name, command)
21
+ end
22
+ end
23
+ end
24
+
25
+ def register(name, command)
26
+ @list[name.to_s] = Command.new(name, command)
27
+ self
28
+ end
29
+
30
+ private
31
+
32
+ private_class_method def self.name_for_item(item, id:)
33
+ case id
34
+ when Symbol
35
+ item.send(id)
36
+ when Proc
37
+ id.call(item)
38
+ else
39
+ raise ArgumentError, "What is #{id.inspect}?!"
40
+ end
41
+ end
42
+
43
+ def run
44
+ if @sequential
45
+ run_sequentially
46
+ else
47
+ run_concurrently
48
+ end
49
+ end
50
+
51
+ def run_concurrently
52
+ statuses = []
53
+ with_io_handler do |io_handler|
54
+ commands.each(&:run)
55
+ threads = commands.map do |command|
56
+ Thread.new do
57
+ pid_and_status = ::Process.wait2(command.pid)
58
+ io_handler.handle_exit_statuses!([pid_and_status], commands)
59
+ statuses << pid_and_status
60
+ end
61
+ end
62
+ threads.each(&:join)
63
+ end
64
+ verify_exit_statuses(statuses, commands)
65
+ end
66
+
67
+ def run_sequentially
68
+ with_io_handler do |io_handler|
69
+ commands.each do |command|
70
+ command.run
71
+ statuses = ::Process.waitall
72
+ io_handler.handle_exit_statuses!(statuses, commands)
73
+ verify_exit_statuses(statuses, commands)
74
+ end
75
+ end
76
+ end
77
+
78
+ def with_io_handler
79
+ io_handler = @io_handler_class.new(ios)
80
+ io_handler.start
81
+ thread = Thread.new { io_handler.handle_io! }
82
+ result = yield io_handler
83
+ thread.terminate
84
+ io_handler.stop
85
+ result
86
+ end
87
+
88
+ def ios
89
+ commands.flat_map(&:ios)
90
+ end
91
+
92
+ def commands
93
+ @list.values
94
+ end
95
+
96
+ def verify_exit_statuses(statuses, commands)
97
+ failure_reports = statuses
98
+ .map(&:last)
99
+ .reject(&:success?)
100
+ .map { |status| CommandReport.gather(status, commands) }
101
+ return if failure_reports.none?
102
+
103
+ warn ""
104
+ warn failure_reports.join("\n")
105
+ raise Failure, "Not every process was successful; #{failure_reports.size} failed"
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,55 @@
1
+ require "concurrent/array"
2
+
3
+ module Plywood
4
+ class FancyLogger < IOHandler
5
+ class FancyLog
6
+ attr_reader :exit_status, :name, :output
7
+
8
+ def initialize(name)
9
+ @name = name
10
+ @exit_status = nil
11
+ @output = Concurrent::Array.new
12
+ end
13
+
14
+ def add_line(stream)
15
+ raise "Process exited" unless exit_status.nil?
16
+ stream.gets.each_line do |line|
17
+ @output << [(stream.err? ? :stderr : :stdout), line]
18
+ end
19
+ self
20
+ end
21
+
22
+ def exit_status=(status)
23
+ @exit_status = status.to_i
24
+ end
25
+
26
+ def running?
27
+ @exit_status.nil?
28
+ end
29
+
30
+ def success?
31
+ @exit_status&.zero?
32
+ end
33
+
34
+ def to_s
35
+ "#{icon} #{name} | #{status}"
36
+ end
37
+
38
+ def status
39
+ case exit_status
40
+ when nil then "running"
41
+ when 0 then "success"
42
+ else "failed (#{exit_status})"
43
+ end
44
+ end
45
+
46
+ def icon
47
+ case exit_status
48
+ when nil then "⏵"
49
+ when 0 then "✔"
50
+ else "‼"
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,44 @@
1
+ require "io/console"
2
+
3
+ module Plywood
4
+ class FancyLogger < IOHandler
5
+ class Frame
6
+ def initialize(io, rows = nil, cols = nil)
7
+ rows, cols = IO.console.winsize if rows.nil? || cols.nil?
8
+ @io = io
9
+ @rows = rows.to_i - 1
10
+ @cols = cols.to_i
11
+ @lines = []
12
+ end
13
+
14
+ def self.render(*, &)
15
+ frame = new(*)
16
+ yield frame
17
+ frame.render
18
+ end
19
+
20
+ def line(string, color = nil)
21
+ return if @lines.size >= @rows
22
+ string = string.to_s[0, @cols].ljust(@cols)
23
+ string = @io.color(color) + string + @io.color(:reset) if color
24
+ @lines << string
25
+ self
26
+ end
27
+
28
+ def render
29
+ @lines << "".ljust(@cols) until @lines.size == @rows
30
+ @lines.each.with_index do |line, index|
31
+ # position_cursor(@rows - @lines.size + index + 1, 1)
32
+ position_cursor(index + 1, 1)
33
+ @io << line
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def position_cursor(row, col)
40
+ @io.write "#{CSI}#{row};#{col}H"
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,150 @@
1
+ require_relative "color"
2
+ require_relative "io_handler"
3
+ require_relative "fancy_logger/fancy_log"
4
+ require_relative "fancy_logger/frame"
5
+
6
+ module Plywood
7
+ class FancyLogger < IOHandler
8
+ CSI = "\e[".freeze
9
+ FPS = 20 # frames per second
10
+ STOP_DELAY = 0.6 # seconds
11
+
12
+ def initialize(ios)
13
+ super
14
+ @names = ios.map(&:name).uniq
15
+ @logs = @names
16
+ .map { |name, _index| [name, FancyLog.new(name)] }
17
+ .to_h
18
+ Color.enable($stdout)
19
+ end
20
+
21
+ def start
22
+ @initial_console_mode = $stdout.console_mode
23
+ IO.console.raw!
24
+ hide_cursor
25
+ use_alternate_screen_buffer
26
+ @render_thread = Thread.new do
27
+ loop do
28
+ render
29
+ sleep(1.0 / FPS)
30
+ end
31
+ end
32
+ self
33
+ end
34
+
35
+ def stop
36
+ sleep STOP_DELAY
37
+ Thread.kill(@render_thread) unless @render_thread.nil?
38
+ # IO.console.cooked!
39
+ $stdout.console_mode = @initial_console_mode
40
+ use_main_screen_buffer
41
+ show_cursor
42
+ render_report
43
+ self
44
+ end
45
+
46
+ def handle_io!
47
+ loop do
48
+ readers, _writers = IO.select(@ios)
49
+ readers.each { |stream| copy_stream(stream) }
50
+ end
51
+ self
52
+ end
53
+
54
+ def handle_exit_statuses!(statuses, commands)
55
+ statuses.each do |pid, status|
56
+ command = commands.detect { |item| item.pid == pid }
57
+ @logs[command.name].exit_status = status
58
+ end
59
+ self
60
+ end
61
+
62
+ private
63
+
64
+ def copy_stream(stream)
65
+ if stream.eof?
66
+ @ios.delete(stream)
67
+ return self
68
+ end
69
+ @logs[stream.name].add_line(stream)
70
+ self
71
+ end
72
+
73
+ def render
74
+ Frame.render($stdout) do |frame|
75
+ @logs.each_value do |log|
76
+ frame.line(log, color(log.name))
77
+ next unless log.running?
78
+ gather_output(log).each do |line|
79
+ frame.line(line)
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ # show last x lines of output
86
+ def gather_output(log, amount = 8)
87
+ lines = log.output.last(amount).map(&:last)
88
+ lines << "" until lines.size == amount
89
+ lines
90
+ end
91
+
92
+ def render_report
93
+ render_success_report
94
+ render_failures_report
95
+ end
96
+
97
+ def render_success_report
98
+ @logs.each_value do |log|
99
+ next unless log.success?
100
+ header = $stdout.color color(log.name)
101
+ header << log.to_s
102
+ header << $stdout.color(:reset)
103
+ puts header
104
+ end
105
+ end
106
+
107
+ def render_failures_report
108
+ @logs.each_value do |log|
109
+ next if log.success?
110
+ puts
111
+ $stdout.puts failure_log_header(log)
112
+ log.output.each do |line|
113
+ case line.first
114
+ when :stdout
115
+ $stdout.puts line.last
116
+ when :stderr
117
+ $stderr.puts line.last
118
+ end
119
+ end
120
+ end
121
+ end
122
+
123
+ def failure_log_header(log)
124
+ $stdout.color(color(log.name)) << "-" * 40 << "\n" << log.to_s << $stdout.color(:reset)
125
+ end
126
+
127
+ COLORS = %w[cyan yellow green magenta red blue bright_cyan bright_yellow
128
+ bright_green bright_magenta bright_red bright_blue].freeze
129
+
130
+ def color(name)
131
+ COLORS[@names.index(name) % COLORS.length]
132
+ end
133
+
134
+ def use_alternate_screen_buffer
135
+ $stdout.write "#{CSI}?1049h"
136
+ end
137
+
138
+ def use_main_screen_buffer
139
+ $stdout.write "#{CSI}?1049l"
140
+ end
141
+
142
+ def hide_cursor
143
+ $stdout.write "#{CSI}?25l"
144
+ end
145
+
146
+ def show_cursor
147
+ $stdout.write "#{CSI}?25h"
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,19 @@
1
+ module Plywood
2
+ class IOHandler
3
+ def initialize(ios)
4
+ @ios = ios
5
+ end
6
+
7
+ def handle_io!
8
+ end
9
+
10
+ def handle_exit_statuses!(statuses)
11
+ end
12
+
13
+ def start
14
+ end
15
+
16
+ def stop
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,79 @@
1
+ require_relative "color"
2
+ require_relative "io_handler"
3
+
4
+ module Plywood
5
+ # An IOHandler that outputs all process output, line by line, prefixed with "name | ".
6
+ #
7
+ # It keeps stderr and stdout separated which may be convenient if you want to pipe
8
+ # the output to another program.
9
+ class Logger < IOHandler
10
+ def initialize(ios)
11
+ super
12
+ @name_padding = [6, ios.map(&:name).map(&:size).max].max
13
+ @names = ios.map(&:name).uniq
14
+ Color.enable($stderr)
15
+ Color.enable($stdout)
16
+ end
17
+
18
+ def handle_io!
19
+ loop do
20
+ readers, _writers = IO.select(@ios)
21
+ readers.each { |stream| copy_stream_to_io(stream) }
22
+ end
23
+ self
24
+ end
25
+
26
+ def handle_exit_statuses!(statuses, commands)
27
+ statuses.each do |pid, status|
28
+ next if status.success?
29
+ command = commands.detect { |item| item.pid == pid }
30
+ log($stderr, command.name, "`#{command}` failed with status #{status.to_i}")
31
+ end
32
+ self
33
+ end
34
+
35
+ private
36
+
37
+ def copy_stream_to_io(stream)
38
+ if stream.eof?
39
+ @ios.delete(stream)
40
+ return self
41
+ end
42
+
43
+ io = stream.err? ? $stderr : $stdout
44
+ log(io, stream.name, stream.gets)
45
+ self
46
+ end
47
+
48
+ def log(io, name, string)
49
+ output = io.color color(name)
50
+ output << name.ljust(@name_padding, " ") << " | "
51
+ output << io.color(:reset)
52
+ output << string
53
+ io.puts output
54
+ io.flush
55
+ end
56
+
57
+ def name_process_and_io(stream)
58
+ @processes.each do |name, process|
59
+ return [name, process, $stderr] if process.stderr == stream
60
+ return [name, process, $stdout] if process.stdout == stream
61
+ end
62
+ raise IndexError
63
+ end
64
+
65
+ def name_and_process(pid)
66
+ @processes.each do |name, process|
67
+ return [name, process] if process.pid == pid
68
+ end
69
+ raise IndexError
70
+ end
71
+
72
+ COLORS = %w[cyan yellow green magenta red blue bright_cyan bright_yellow
73
+ bright_green bright_magenta bright_red bright_blue].freeze
74
+
75
+ def color(name)
76
+ COLORS[@names.index(name) % COLORS.length]
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,31 @@
1
+ require "delegate"
2
+
3
+ module Plywood
4
+ # This class is used as a wrapper around an io.
5
+ # It adds extra methods that are useful to distinguish it from other IOs in an efficient way.
6
+ class NamedIO < SimpleDelegator
7
+ attr_reader :name, :stream
8
+
9
+ def initialize(io, name, stream)
10
+ super(io)
11
+ @name = name.to_s
12
+ @stream = stream.to_sym
13
+ validate_stream
14
+ end
15
+
16
+ def err?
17
+ @stream == :err
18
+ end
19
+
20
+ def out?
21
+ @stream == :out
22
+ end
23
+
24
+ private
25
+
26
+ def validate_stream
27
+ return if [:err, :out].include? @stream
28
+ raise ArgumentError, "stream cannot be #{@stream}"
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,3 @@
1
+ module Plywood
2
+ VERSION = "1.1.1".freeze
3
+ end
data/lib/plywood.rb ADDED
@@ -0,0 +1,5 @@
1
+ require_relative "plywood/commands"
2
+
3
+ module Plywood
4
+ Failure = Class.new(StandardError)
5
+ end
data/logo.jpeg ADDED
Binary file
metadata ADDED
@@ -0,0 +1,173 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: plywood
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Tijn Schuurmans
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-04-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: minitest
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest-focus
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop-minitest
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop-rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: sdoc
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: simplecov
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: spy
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description: Run multiple commands in parallel; consolidate the output
126
+ email: plywood@tijnschuurmans.nl
127
+ executables: []
128
+ extensions: []
129
+ extra_rdoc_files:
130
+ - README.md
131
+ - logo.jpeg
132
+ files:
133
+ - README.md
134
+ - lib/plywood.rb
135
+ - lib/plywood/color.rb
136
+ - lib/plywood/command.rb
137
+ - lib/plywood/command_report.rb
138
+ - lib/plywood/commands.rb
139
+ - lib/plywood/fancy_logger.rb
140
+ - lib/plywood/fancy_logger/fancy_log.rb
141
+ - lib/plywood/fancy_logger/frame.rb
142
+ - lib/plywood/io_handler.rb
143
+ - lib/plywood/logger.rb
144
+ - lib/plywood/named_io.rb
145
+ - lib/plywood/version.rb
146
+ - logo.jpeg
147
+ homepage: https://github.com/Jobport/plywood
148
+ licenses:
149
+ - Copyright 2023 Jobport B.V.
150
+ - MIT
151
+ metadata:
152
+ homepage_uri: https://github.com/Jobport/plywood
153
+ changelog_uri: https://github.com/Jobport/plywood/releases
154
+ post_install_message:
155
+ rdoc_options: []
156
+ require_paths:
157
+ - lib
158
+ required_ruby_version: !ruby/object:Gem::Requirement
159
+ requirements:
160
+ - - ">="
161
+ - !ruby/object:Gem::Version
162
+ version: 3.3.0
163
+ required_rubygems_version: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - ">="
166
+ - !ruby/object:Gem::Version
167
+ version: '0'
168
+ requirements: []
169
+ rubygems_version: 3.5.22
170
+ signing_key:
171
+ specification_version: 4
172
+ summary: The simplest multiplexer
173
+ test_files: []