console 1.29.2 → 1.36.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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/context/command-line.md +47 -0
  4. data/context/configuration.md +23 -0
  5. data/context/events.md +71 -0
  6. data/context/getting-started.md +170 -0
  7. data/context/index.yaml +28 -0
  8. data/context/integration.md +37 -0
  9. data/lib/console/capture.rb +29 -3
  10. data/lib/console/clock.rb +9 -3
  11. data/lib/console/compatible/logger.rb +34 -3
  12. data/lib/console/config.rb +96 -0
  13. data/lib/console/event/failure.rb +33 -4
  14. data/lib/console/event/generic.rb +22 -1
  15. data/lib/console/event/spawn.rb +39 -7
  16. data/lib/console/event.rb +7 -1
  17. data/lib/console/filter.rb +83 -7
  18. data/lib/console/format/safe.rb +205 -58
  19. data/lib/console/format.rb +5 -1
  20. data/lib/console/interface.rb +18 -10
  21. data/lib/console/logger.rb +25 -38
  22. data/lib/console/output/default.rb +31 -6
  23. data/lib/console/output/failure.rb +9 -2
  24. data/lib/console/output/null.rb +5 -2
  25. data/lib/console/output/sensitive.rb +42 -1
  26. data/lib/console/output/serialized.rb +32 -7
  27. data/lib/console/output/split.rb +12 -1
  28. data/lib/console/output/terminal.rb +68 -15
  29. data/lib/console/output/wrapper.rb +12 -1
  30. data/lib/console/output.rb +14 -3
  31. data/lib/console/progress.rb +56 -9
  32. data/lib/console/resolver.rb +19 -2
  33. data/lib/console/terminal/formatter/failure.rb +18 -6
  34. data/lib/console/terminal/formatter/progress.rb +16 -3
  35. data/lib/console/terminal/formatter/spawn.rb +16 -5
  36. data/lib/console/terminal/formatter.rb +16 -0
  37. data/lib/console/terminal/text.rb +66 -20
  38. data/lib/console/terminal/xterm.rb +18 -3
  39. data/lib/console/terminal.rb +7 -9
  40. data/lib/console/version.rb +2 -2
  41. data/lib/console/warn.rb +2 -2
  42. data/lib/console.rb +2 -1
  43. data/license.md +6 -3
  44. data/readme.md +60 -6
  45. data/releases.md +115 -1
  46. data.tar.gz.sig +0 -0
  47. metadata +20 -12
  48. metadata.gz.sig +0 -0
@@ -1,29 +1,179 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2023-2024, by Samuel Williams.
4
+ # Copyright, 2023-2025, by Samuel Williams.
5
5
 
6
6
  require "json"
7
7
 
8
8
  module Console
9
+ # @namespace
9
10
  module Format
10
- # This class is used to safely dump objects.
11
- # It will attempt to dump the object using the given format, but if it fails, it will generate a safe version of the object.
11
+ # A safe format for converting objects to strings.
12
+ #
13
+ # Handles issues like circular references, encoding errors, excessive nesting depth, and excessive output size.
12
14
  class Safe
13
- def initialize(format: ::JSON, limit: 8, encoding: ::Encoding::UTF_8)
15
+ # The JSON fragment used as the truncation marker when dropped fields cannot be named.
16
+ TRUNCATED = "\"truncated\":true"
17
+
18
+ # Create a new safe format.
19
+ #
20
+ # @parameter format [JSON] The format to use for serialization.
21
+ # @parameter depth_limit [Integer] The maximum depth to recurse into objects (the JSON `max_nesting`).
22
+ # @parameter size_limit [Integer | Nil] The maximum byte size of the serialized output, or `nil` to disable size limiting. Limits below {TRUNCATED} (the minimal marker) cannot be honoured.
23
+ # @parameter encoding [Encoding] The encoding to use for strings.
24
+ # @parameter limit [Integer | Nil] Deprecated alias for `depth_limit`.
25
+ def initialize(format: ::JSON, depth_limit: 12, size_limit: 16 * 1024, encoding: ::Encoding::UTF_8, limit: nil)
26
+ if limit
27
+ warn "Console::Format::Safe `limit:` is deprecated, use `depth_limit:` instead.", uplevel: 1, category: :deprecated
28
+ depth_limit = limit
29
+ end
30
+
14
31
  @format = format
15
- @limit = limit
32
+ @depth_limit = depth_limit
33
+ @size_limit = size_limit
16
34
  @encoding = encoding
17
35
  end
18
36
 
37
+ # @attribute [Integer] The maximum depth to recurse into objects.
38
+ attr :depth_limit
39
+
40
+ # @attribute [Integer | Nil] The maximum byte size of the serialized output.
41
+ attr :size_limit
42
+
43
+ # Dump the given object to a string.
44
+ #
45
+ # The common case is a single fast serialization. If that fails (e.g. circular
46
+ # references, excessive nesting, or encoding errors) or its output exceeds
47
+ # {size_limit}, it falls back to {safe_dump}, which rebuilds the record
48
+ # field-by-field within the limit.
49
+ #
50
+ # @parameter object [Object] The object to dump.
51
+ # @returns [String] The dumped object.
19
52
  def dump(object)
20
- @format.dump(object, @limit)
21
- rescue SystemStackError, StandardError => error
22
- @format.dump(safe_dump(object, error))
53
+ buffer = @format.dump(object, @depth_limit)
54
+
55
+ if @size_limit and buffer.bytesize > @size_limit
56
+ return safe_dump(object)
57
+ end
58
+
59
+ return buffer
60
+ rescue SystemStackError, StandardError
61
+ return safe_dump(object)
23
62
  end
24
63
 
25
64
  private
26
65
 
66
+ # Produce a safe, size-limited serialization of the given object. This is the
67
+ # fallback path, used both when direct serialization fails (an exception) and
68
+ # when its output exceeds {size_limit}.
69
+ #
70
+ # Each top-level value is serialized independently and defensively, so a single
71
+ # un-serializable or oversized value cannot break or bloat the whole record.
72
+ # Whenever a field is degraded, the reason is recorded in a trailing `"truncated"`
73
+ # object that maps the field name to why it was truncated:
74
+ #
75
+ # - `"key": true` — the value was dropped because it did not fit the size limit.
76
+ # - `"key": {error}` — the value could not be serialized directly; a safe
77
+ # representation was kept in its place and the triggering error is recorded.
78
+ #
79
+ # Fields are kept while they fit, always reserving room for at least a minimal
80
+ # `"truncated":true` marker. The detailed reason map is then emitted only if it
81
+ # fits in the remaining space; otherwise it degrades to `"truncated":true`. This
82
+ # is best-effort — in the worst case the per-field detail is lost — but it keeps
83
+ # the bookkeeping simple and the size guarantee hard.
84
+ #
85
+ # @parameter object [Object] The object to serialize.
86
+ # @returns [String] The safe, size-limited serialized record.
87
+ def safe_dump(object)
88
+ # Serialize hash-like objects field-by-field; anything else falls through to the
89
+ # error handler below, which emits a minimal truncated marker.
90
+ object = object.to_hash
91
+
92
+ # Serialize each field once, capturing the error for any value that could not be
93
+ # serialized directly. Our own "truncated" key is skipped so it is never duplicated.
94
+ errors = {}
95
+ fragments = []
96
+ object.each do |key, value|
97
+ name = key.to_s
98
+ next if name == "truncated"
99
+
100
+ fragment, error = dump_pair(key, value)
101
+ errors[name] = error_info(error) if error
102
+ fragments << [name, fragment]
103
+ end
104
+
105
+ # Assemble the body, keeping each field while it fits — always reserving room for
106
+ # at least a minimal `"truncated":true` marker. Each truncated field's reason is
107
+ # collected: its error (value recovered) or `true` (dropped for size).
108
+ buffer = +"{"
109
+ first = true
110
+ reasons = {}
111
+
112
+ fragments.each do |name, fragment|
113
+ if buffer.bytesize + (first ? 0 : 1) + fragment.bytesize + TRUNCATED.bytesize + 2 <= @size_limit
114
+ buffer << "," unless first
115
+ buffer << fragment
116
+ first = false
117
+
118
+ # The value was kept; if it had to be recovered, note why.
119
+ reasons[name] = errors[name] if errors[name]
120
+ else
121
+ # The value did not fit and was dropped entirely.
122
+ reasons[name] = true
123
+ end
124
+ end
125
+
126
+ unless reasons.empty?
127
+ # Include the detailed reasons if they fit, otherwise fall back to the minimal
128
+ # marker so the truncation is still signalled.
129
+ detailed = "\"truncated\":#{@format.dump(reasons)}"
130
+ fits = buffer.bytesize + (first ? 0 : 1) + detailed.bytesize + 1 <= @size_limit
131
+
132
+ buffer << "," unless first
133
+ buffer << (fits ? detailed : TRUNCATED)
134
+ end
135
+
136
+ buffer << "}"
137
+
138
+ return buffer
139
+ rescue SystemStackError, StandardError
140
+ return "{#{TRUNCATED}}"
141
+ end
142
+
143
+ # Serialize a single top-level `"key":value` pair, safely handling values that
144
+ # cannot be serialized directly.
145
+ #
146
+ # @parameter key [Object] The field key.
147
+ # @parameter value [Object] The field value.
148
+ # @returns [Array(String, Exception | Nil)] The `"key":value` fragment and the error, if recovery was needed.
149
+ def dump_pair(key, value)
150
+ value_json, error = dump_value(value)
151
+
152
+ return ["#{dump_string(String(key))}:#{value_json}", error]
153
+ end
154
+
155
+ # Serialize a single value, falling back to a safe representation on failure.
156
+ #
157
+ # @parameter value [Object] The value to serialize.
158
+ # @returns [Array(String, Exception | Nil)] The serialized value and the error, if recovery was needed.
159
+ def dump_value(value)
160
+ [@format.dump(value, @depth_limit), nil]
161
+ rescue SystemStackError, StandardError => error
162
+ [@format.dump(safe_dump_recurse(value)), error]
163
+ end
164
+
165
+ # Serialize a string as a JSON string, encoding it safely first.
166
+ #
167
+ # @parameter value [String] The string to serialize.
168
+ # @returns [String] The serialized (quoted) string.
169
+ def dump_string(value)
170
+ @format.dump(value.encode(@encoding, invalid: :replace, undef: :replace))
171
+ end
172
+
173
+ # Filter the backtrace to remove duplicate frames and reduce verbosity.
174
+ #
175
+ # @parameter error [Exception] The exception to filter.
176
+ # @returns [Array(String)] The filtered backtrace.
27
177
  def filter_backtrace(error)
28
178
  frames = error.backtrace
29
179
  filtered = {}
@@ -61,74 +211,71 @@ module Console
61
211
  return frames
62
212
  end
63
213
 
64
- def safe_dump(object, error)
65
- object = safe_dump_recurse(object)
66
-
67
- object[:truncated] = true
68
- object[:error] = {
214
+ # Build a safe, primitive representation of an error for inclusion as an `"error"` field.
215
+ #
216
+ # @parameter error [Exception] The error that occurred while dumping the object.
217
+ # @returns [Hash] The error details (class, message, filtered backtrace).
218
+ def error_info(error)
219
+ {
69
220
  class: safe_dump_recurse(error.class.name),
70
221
  message: safe_dump_recurse(error.message),
71
222
  backtrace: safe_dump_recurse(filter_backtrace(error)),
72
223
  }
73
-
74
- return object
75
- end
76
-
77
- def replacement_for(object)
78
- case object
79
- when Array
80
- "[...]"
81
- when Hash
82
- "{...}"
83
- else
84
- "..."
85
- end
86
224
  end
87
225
 
226
+ # Create a new hash with identity comparison.
88
227
  def default_objects
89
228
  Hash.new.compare_by_identity
90
229
  end
91
230
 
92
- # This will recursively generate a safe version of the object.
93
- # Nested hashes and arrays will be transformed recursively.
94
- # Strings will be encoded with the given encoding.
95
- # Primitive values will be returned as-is.
96
- # Other values will be converted using `as_json` if available, otherwise `to_s`.
97
- def safe_dump_recurse(object, limit = @limit, objects = default_objects)
98
- if limit <= 0 || objects[object]
99
- return replacement_for(object)
100
- end
101
-
231
+ # This will recursively generate a safe version of the object. Nested hashes and arrays will be transformed recursively. Strings will be encoded with the given encoding. Primitive values will be returned as-is. Other values will be converted using `as_json` if available, otherwise `to_s`.
232
+ #
233
+ # @parameter object [Object] The object to dump.
234
+ # @parameter limit [Integer] The maximum depth to recurse into objects.
235
+ # @parameter objects [Hash] The objects that have already been visited.
236
+ # @returns [Object] The dumped object as a primitive representation.
237
+ def safe_dump_recurse(object, limit = @depth_limit, objects = default_objects)
102
238
  case object
103
239
  when Hash
104
- objects[object] = true
105
-
106
- object.to_h do |key, value|
107
- [
108
- String(key).encode(@encoding, invalid: :replace, undef: :replace),
109
- safe_dump_recurse(value, limit - 1, objects)
110
- ]
240
+ if limit <= 0 || objects[object]
241
+ return "{...}"
242
+ else
243
+ objects[object] = true
244
+
245
+ return object.to_h do |key, value|
246
+ [
247
+ String(key).encode(@encoding, invalid: :replace, undef: :replace),
248
+ safe_dump_recurse(value, limit - 1, objects)
249
+ ]
250
+ end
111
251
  end
112
252
  when Array
113
- objects[object] = true
114
-
115
- object.map do |value|
116
- safe_dump_recurse(value, limit - 1, objects)
253
+ if limit <= 0 || objects[object]
254
+ return "[...]"
255
+ else
256
+ objects[object] = true
257
+
258
+ return object.map do |value|
259
+ safe_dump_recurse(value, limit - 1, objects)
260
+ end
117
261
  end
118
262
  when String
119
- object.encode(@encoding, invalid: :replace, undef: :replace)
263
+ return object.encode(@encoding, invalid: :replace, undef: :replace)
120
264
  when Numeric, TrueClass, FalseClass, NilClass
121
- object
265
+ return object
122
266
  else
123
- objects[object] = true
124
-
125
- # We could do something like this but the chance `as_json` will blow up.
126
- # We'd need to be extremely careful about it.
127
- # if object.respond_to?(:as_json)
128
- # safe_dump_recurse(object.as_json, limit - 1, objects)
129
- # else
130
-
131
- safe_dump_recurse(object.to_s, limit - 1, objects)
267
+ if limit <= 0 || objects[object]
268
+ return "..."
269
+ else
270
+ objects[object] = true
271
+
272
+ # We could do something like this but the chance `as_json` will blow up.
273
+ # We'd need to be extremely careful about it.
274
+ # if object.respond_to?(:as_json)
275
+ # safe_dump_recurse(object.as_json, limit - 1, objects)
276
+ # else
277
+ return safe_dump_recurse(object.to_s, limit - 1, objects)
278
+ end
132
279
  end
133
280
  end
134
281
  end
@@ -1,12 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2023-2024, by Samuel Williams.
4
+ # Copyright, 2023-2025, by Samuel Williams.
5
5
 
6
6
  require_relative "format/safe"
7
7
 
8
8
  module Console
9
+ # @namespace
9
10
  module Format
11
+ # A safe format for converting objects to strings.
12
+ #
13
+ # @returns [Console::Format::Safe]
10
14
  def self.default
11
15
  Safe.new(format: ::JSON)
12
16
  end
@@ -1,53 +1,61 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright 2024, by Samuel Williams.
4
+ # Copyright, 2024-2025, by Samuel Williams.
5
5
 
6
- require_relative "logger"
6
+ require "fiber/local"
7
+ require_relative "config"
7
8
 
8
9
  module Console
9
10
  # The public logger interface.
10
11
  module Interface
12
+ extend Fiber::Local
13
+
14
+ # Create a new (thread local) logger instance.
15
+ def self.local
16
+ Config::DEFAULT.make_logger
17
+ end
18
+
11
19
  # Get the current logger instance.
12
20
  def logger
13
- Logger.instance
21
+ Interface.instance
14
22
  end
15
23
 
16
24
  # Set the current logger instance.
17
25
  #
18
26
  # The current logger instance is assigned per-fiber.
19
27
  def logger= instance
20
- Logger.instance= instance
28
+ Interface.instance= instance
21
29
  end
22
30
 
23
31
  # Emit a debug log message.
24
32
  def debug(...)
25
- Logger.instance.debug(...)
33
+ Interface.instance.debug(...)
26
34
  end
27
35
 
28
36
  # Emit an informational log message.
29
37
  def info(...)
30
- Logger.instance.info(...)
38
+ Interface.instance.info(...)
31
39
  end
32
40
 
33
41
  # Emit a warning log message.
34
42
  def warn(...)
35
- Logger.instance.warn(...)
43
+ Interface.instance.warn(...)
36
44
  end
37
45
 
38
46
  # Emit an error log message.
39
47
  def error(...)
40
- Logger.instance.error(...)
48
+ Interface.instance.error(...)
41
49
  end
42
50
 
43
51
  # Emit a fatal log message.
44
52
  def fatal(...)
45
- Logger.instance.fatal(...)
53
+ Interface.instance.fatal(...)
46
54
  end
47
55
 
48
56
  # Emit a log message with arbitrary arguments and options.
49
57
  def call(...)
50
- Logger.instance.call(...)
58
+ Interface.instance.call(...)
51
59
  end
52
60
  end
53
61
  end
@@ -1,68 +1,49 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2024, by Samuel Williams.
4
+ # Copyright, 2019-2025, by Samuel Williams.
5
5
  # Copyright, 2021, by Bryan Powell.
6
6
  # Copyright, 2021, by Robert Schulze.
7
+ # Copyright, 2025, by Shigeru Nakajima.
8
+
9
+ require_relative "filter"
7
10
 
8
11
  require_relative "output"
9
12
  require_relative "output/failure"
10
13
 
11
- require_relative "filter"
12
- require_relative "event"
13
- require_relative "resolver"
14
14
  require_relative "progress"
15
15
 
16
- require "fiber/local"
17
-
18
16
  module Console
17
+ # The standard logger interface with support for log levels and verbosity.
18
+ #
19
+ # The log levels are: `debug`, `info`, `warn`, `error`, and `fatal`.
19
20
  class Logger < Filter[debug: 0, info: 1, warn: 2, error: 3, fatal: 4]
20
- extend Fiber::Local
21
-
22
21
  # Set the default log level based on `$DEBUG` and `$VERBOSE`.
23
22
  # You can also specify CONSOLE_LEVEL=debug or CONSOLE_LEVEL=info in environment.
24
23
  # https://mislav.net/2011/06/ruby-verbose-mode/ has more details about how it all fits together.
25
- def self.default_log_level(env = ENV)
24
+ #
25
+ # @parameter env [Hash] The environment to read the log level from.
26
+ # @parameter verbose [Boolean] The verbose flag.
27
+ # @parameter debug [Boolean] The debug flag.
28
+ # @returns [Integer] The default log level.
29
+ def self.default_log_level(env = ENV, verbose: $VERBOSE, debug: $DEBUG)
26
30
  if level = env["CONSOLE_LEVEL"]
27
31
  LEVELS[level.to_sym] || level.to_i
28
- elsif $DEBUG
32
+ elsif debug
29
33
  DEBUG
30
- elsif $VERBOSE.nil?
34
+ elsif verbose.nil?
31
35
  WARN
32
36
  else
33
37
  INFO
34
38
  end
35
39
  end
36
40
 
37
- # Controls verbose output using `$VERBOSE`.
38
- def self.verbose?(env = ENV)
39
- !$VERBOSE.nil? || env["CONSOLE_VERBOSE"]
40
- end
41
-
42
- def self.default_logger(output = $stderr, env = ENV, **options)
43
- if options[:verbose].nil?
44
- options[:verbose] = self.verbose?(env)
45
- end
46
-
47
- if options[:level].nil?
48
- options[:level] = self.default_log_level(env)
49
- end
50
-
51
- output = Output.new(output, env, **options)
52
-
53
- logger = self.new(output, **options)
54
-
55
- Resolver.default_resolver(logger)
56
-
57
- return logger
58
- end
59
-
60
- def self.local
61
- self.default_logger
62
- end
63
-
64
41
  DEFAULT_LEVEL = 1
65
42
 
43
+ # Create a new logger.
44
+ #
45
+ # @parameter output [Console::Output] The output destination.
46
+ # @parameter options [Hash] Additional options.
66
47
  def initialize(output, **options)
67
48
  # This is the expected default behaviour, but it may be nice to have a way to override it.
68
49
  output = Output::Failure.new(output, **options)
@@ -70,6 +51,12 @@ module Console
70
51
  super(output, **options)
71
52
  end
72
53
 
54
+ # Create a progress indicator for the given subject.
55
+ #
56
+ # @parameter subject [String] The subject of the progress indicator.
57
+ # @parameter total [Integer] The total number of items to process.
58
+ # @parameter options [Hash] Additional options passed to {Progress}.
59
+ # @returns [Progress] The progress indicator.
73
60
  def progress(subject, total, **options)
74
61
  options[:severity] ||= :info
75
62
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2021-2024, by Samuel Williams.
4
+ # Copyright, 2021-2025, by Samuel Williams.
5
5
 
6
6
  require_relative "terminal"
7
7
  require_relative "serialized"
@@ -9,18 +9,43 @@ require_relative "failure"
9
9
 
10
10
  module Console
11
11
  module Output
12
+ # Default output format selection.
12
13
  module Default
13
- def self.new(output, **options)
14
- output ||= $stderr
14
+ # Create a new output format based on the given stream.
15
+ #
16
+ # @parameter io [IO] The output stream.
17
+ # @parameter env [Hash] Environment variables (defaults to ENV for testing).
18
+ # @parameter options [Hash] Additional options to customize the output.
19
+ # @returns [Console::Output::Terminal | Console::Output::Serialized] The output instance, depending on whether the `io` is a terminal or not.
20
+ def self.new(stream, env: ENV, **options)
21
+ stream ||= $stderr
15
22
 
16
- if output.tty?
17
- output = Terminal.new(output, **options)
23
+ if stream.tty?
24
+ output = Terminal.new(stream, **options)
25
+ elsif self.mail?(env)
26
+ output = Text.new(stream, **options)
27
+ elsif self.github_actions?(env)
28
+ output = XTerm.new(stream, **options)
18
29
  else
19
- output = Serialized.new(output, **options)
30
+ output = Serialized.new(stream, **options)
20
31
  end
21
32
 
22
33
  return output
23
34
  end
35
+
36
+ private
37
+
38
+ # Detect if we're running in a cron job or mail context where human-readable output is preferred.
39
+ # Cron jobs often have MAILTO set and lack TERM, or have minimal TERM values.
40
+ def self.mail?(env = ENV)
41
+ env.key?("MAILTO") && !env["MAILTO"].empty?
42
+ end
43
+
44
+ # Detect if we're running in GitHub Actions, where human-readable output is preferred.
45
+ # GitHub Actions sets the GITHUB_ACTIONS environment variable to "true".
46
+ def self.github_actions?(env = ENV)
47
+ env["GITHUB_ACTIONS"] == "true"
48
+ end
24
49
  end
25
50
  end
26
51
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2021-2024, by Samuel Williams.
4
+ # Copyright, 2024-2025, by Samuel Williams.
5
5
 
6
6
  require_relative "wrapper"
7
7
  require_relative "../event/failure"
@@ -10,11 +10,18 @@ module Console
10
10
  module Output
11
11
  # A wrapper for outputting failure messages, which can include exceptions.
12
12
  class Failure < Wrapper
13
+ # Create a new failure output wrapper.
13
14
  def initialize(output, **options)
14
15
  super(output, **options)
15
16
  end
16
17
 
17
18
  # The exception must be either the last argument or passed as an option.
19
+ #
20
+ # @parameter subject [String] The subject of the message.
21
+ # @parameter arguments [Array] The arguments to output.
22
+ # @parameter exception [Exception] The exception to output.
23
+ # @parameter options [Hash] Additional options to pass to the output.
24
+ # @parameter block [Proc] An optional block to pass to the output.
18
25
  def call(subject = nil, *arguments, exception: nil, **options, &block)
19
26
  if exception.nil?
20
27
  last = arguments.last
@@ -28,7 +35,7 @@ module Console
28
35
  options[:exception] = exception
29
36
  end
30
37
 
31
- super(subject, *arguments, **options)
38
+ super(subject, *arguments, **options, &block)
32
39
  end
33
40
  end
34
41
  end
@@ -1,20 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2023-2024, by Samuel Williams.
4
+ # Copyright, 2023-2025, by Samuel Williams.
5
5
 
6
6
  module Console
7
7
  module Output
8
+ # A null output that does nothing.
8
9
  class Null
10
+ # Create a new null output.
9
11
  def initialize(...)
10
12
  end
11
13
 
14
+ # The last output is always self.
12
15
  def last_output
13
16
  self
14
17
  end
15
18
 
19
+ # Do nothing.
16
20
  def call(...)
17
- # Do nothing.
18
21
  end
19
22
  end
20
23
  end