pretty_irb 0.1.2 → 0.1.4

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: 30ecfb0481118f36e1674ff5dd9174fbd1e678b50743a6ea03cab465dd2c800e
4
- data.tar.gz: a5fd1711ecdd51ee203546ea3a12f96b5a2ec268e4711c85f1ea0045e55311e2
3
+ metadata.gz: 95ec3de4041e5f89096a2fe2f3a2c34506bde577008b8539d6d5459067d0b360
4
+ data.tar.gz: 2277edc3c5e7913d3537b219e77bc0f0fd85d33c47a259bcc50a57f84f542296
5
5
  SHA512:
6
- metadata.gz: 2d45064a505563e76cc975821c9caf2745885306a50e450e6dafebce7c9ec73270a37201151436db83caf1fe67502bcb8763e216910828b48fd2da6a9095e965
7
- data.tar.gz: 64623e8b5a19298cdb41808c38be3b9f241bd9133b5b0b0354851c7f100a98e5db56ddca2e501ff7ef61f59e637e5f270a3d7045cbe7b95da2bc4eba18fe50a3
6
+ metadata.gz: a7257181d6bf2c0a01212c13cc1ddee99ddda9f2b41131e9ae40b93b1979039b090921494dcd566bfc4d01583e831f37f75812729b008d1f88363f4bc213bb40
7
+ data.tar.gz: b3795b0bcd2e5d7a8ddb5a6bed472b92fd43dbad7f34342bc72709b90ebe95852d986341e7726ad99d3ced9a61a73b5bd8021c5a5de05f6b9732781fa37b6d1f
data/CHANGELOG.md CHANGED
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.1.4] - 2026-11-11
9
+
10
+ ### Added
11
+ - **Rails 7+ Console Integration (`PrettyIRB::Railtie`)** - Automatically wires `config.console` in `rails c` across Rails 7.0, 7.1, 7.2, and 8.x while preserving `reload!`, `app`, `helper`, `controller`, `Rails.backtrace_cleaner`, and `--sandbox`, plus `DISABLE_PRETTY_IRB=1` opt-out
12
+ - **Configuration API (`PrettyIRB::Configuration`)** - Supports `PrettyIRB.configure` and Rails `config.pretty_irb` toggles (`colorize`, `syntax_highlight`, `auto_correct`, `banner`, `rails_env_prompt`)
13
+ - **Terminal UI Toolkit (`PrettyIRB::UI`)** - ANSI-aware alignment, rounded banner/command cards, environment badges (`dev`, `test`, bold red `PROD`, `SANDBOX`), and column-aligned multiline prompt rails
14
+ - **ActiveRecord & Struct Formatting** - Multi-line attribute formatting for `ActiveRecord::Base` and bounded `.limit(31)` formatting for `ActiveRecord::Relation`
15
+ - **Exception Hints & Error History** - `ExceptionHook` on `IRB::Irb#handle_exception` records runtime errors in `HistoryManager` and provides non-duplicate typo / nil-receiver / syntax suggestions
16
+
8
17
  ## [0.1.0] - 2025-11-27
9
18
 
10
19
  ### Added
@@ -286,13 +286,13 @@ module PrettyIRB
286
286
 
287
287
  def example_loops
288
288
  <<~EXAMPLES
289
- šŸ“š Loop Examples:
289
+ šŸ“š Loops and Iteration Examples:
290
290
 
291
291
  Using each:
292
292
  [1, 2, 3].each { |x| puts x }
293
293
 
294
294
  Using times:
295
- 5.times { |i| puts "Number #{i}" }
295
+ 5.times { |i| puts "Number \#{i}" }
296
296
 
297
297
  Using while:
298
298
  i = 0
@@ -707,7 +707,7 @@ module PrettyIRB
707
707
 
708
708
  def general_best_practices
709
709
  <<~PRACTICES
710
- āœ… General Best Practices:
710
+ āœ… General Ruby Best Practices:
711
711
 
712
712
  Code Quality:
713
713
  • DRY: Don't Repeat Yourself
@@ -1,76 +1,87 @@
1
- module PrettyIRB
2
- class AutoCorrector
3
- class << self
4
- # Suggest corrections for common Ruby mistakes
5
- def suggest_corrections(error)
6
- case error
7
- when NameError
8
- suggest_name_error(error)
9
- when NoMethodError
10
- suggest_method_error(error)
11
- when SyntaxError
12
- suggest_syntax_error(error)
13
- else
14
- ""
15
- end
16
- rescue
17
- ""
18
- end
19
-
20
- # Auto-correct common typos in code
21
- def auto_correct_code(code)
22
- code = fix_method_names(code)
23
- code
24
- rescue
25
- code
26
- end
27
-
28
- private
29
-
30
- def suggest_name_error(error)
31
- # Try to get corrections, but safely
32
- begin
33
- if error.respond_to?(:corrections)
34
- corrections = error.corrections
35
- if corrections.is_a?(Array) && corrections.length > 0
36
- "\nšŸ’” Did you mean?: #{corrections.join(', ')}"
37
- else
38
- ""
39
- end
40
- else
41
- ""
42
- end
43
- rescue
44
- ""
45
- end
46
- end
47
-
48
- def suggest_method_error(error)
49
- begin
50
- if error.respond_to?(:corrections)
51
- corrections = error.corrections
52
- if corrections.is_a?(Array) && corrections.length > 0
53
- "\nšŸ’” Did you mean?: #{corrections.join(', ')}"
54
- else
55
- ""
56
- end
57
- else
58
- ""
59
- end
60
- rescue
61
- ""
62
- end
63
- end
64
-
65
- def suggest_syntax_error(error)
66
- ""
67
- end
68
-
69
- def fix_method_names(code)
70
- # Common method name corrections
71
- code.gsub(/\.lenght/, ".length")
72
- .gsub(/\.sizze/, ".size")
73
- end
74
- end
75
- end
76
- end
1
+ module PrettyIRB
2
+ class AutoCorrector
3
+ COMMON_METHOD_TYPOS = {
4
+ "lenght" => "length",
5
+ "sizze" => "size",
6
+ "empyt?" => "empty?",
7
+ "nil" => "nil?",
8
+ "to_str" => "to_s",
9
+ "to_int" => "to_i"
10
+ }.freeze
11
+
12
+ class << self
13
+ # Suggest corrections for common Ruby mistakes
14
+ def suggest_corrections(error)
15
+ case error
16
+ when NameError
17
+ suggest_name_or_method_error(error)
18
+ when SyntaxError
19
+ suggest_syntax_error(error)
20
+ else
21
+ ""
22
+ end
23
+ rescue
24
+ ""
25
+ end
26
+
27
+ # Auto-correct common typos in code
28
+ def auto_correct_code(code)
29
+ fix_method_names(code)
30
+ rescue
31
+ code
32
+ end
33
+
34
+ private
35
+
36
+ def suggest_name_or_method_error(error)
37
+ hints = []
38
+
39
+ if error.respond_to?(:corrections)
40
+ corrections = begin
41
+ error.corrections
42
+ rescue ArgumentError
43
+ nil
44
+ end
45
+ if corrections.is_a?(Array) && !corrections.empty?
46
+ hints << "Did you mean?: #{corrections.join(', ')}"
47
+ end
48
+ end
49
+
50
+ if error.is_a?(NoMethodError)
51
+ method_name = error.name.to_s
52
+ if (replacement = COMMON_METHOD_TYPOS[method_name])
53
+ hints << "Did you mean `.#{replacement}`?"
54
+ end
55
+
56
+ nil_receiver = begin
57
+ error.respond_to?(:receiver) && error.receiver.nil?
58
+ rescue ArgumentError
59
+ false
60
+ end
61
+
62
+ if nil_receiver && !method_name.empty?
63
+ hints << "Receiver is nil — check initialization or use safe navigation (`&.#{method_name}`)"
64
+ end
65
+ end
66
+
67
+ hints.empty? ? "" : "\nšŸ’” #{hints.uniq.join(' Ā· ')}"
68
+ end
69
+
70
+ def suggest_syntax_error(error)
71
+ msg = error.message.to_s
72
+ if msg.include?("unexpected end-of-input") || msg.include?("unterminated")
73
+ "\nšŸ’” Check for a missing `end`, closing bracket, or closing quote."
74
+ elsif msg.include?("unexpected `end'") || msg.include?("unexpected end")
75
+ "\nšŸ’” Extra `end` detected — check block/method nesting."
76
+ else
77
+ ""
78
+ end
79
+ end
80
+
81
+ def fix_method_names(code)
82
+ code.gsub(/\.lenght\b/, ".length")
83
+ .gsub(/\.sizze\b/, ".size")
84
+ end
85
+ end
86
+ end
87
+ end
@@ -2,77 +2,79 @@ require "benchmark"
2
2
 
3
3
  module PrettyIRB
4
4
  class Benchmarker
5
- # Benchmark a code snippet
6
- def self.benchmark(code, iterations = 1000)
7
- begin
8
- time = Benchmark.measure do
9
- iterations.times do
10
- eval(code)
11
- end
5
+ # Benchmark a code snippet, optionally with caller's binding for variable access
6
+ def self.benchmark(code, iterations = 1000, binding = nil)
7
+ time = Benchmark.measure do
8
+ iterations.times do
9
+ binding ? eval(code, binding) : eval(code)
12
10
  end
11
+ end
13
12
 
14
- total_ms = time.real * 1000
15
- per_iteration_us = (total_ms * 1000) / iterations
13
+ total_ms = time.real * 1000
14
+ per_iteration_us = (total_ms * 1000).to_f / iterations
16
15
 
17
- format_benchmark_result(code, iterations, total_ms, per_iteration_us)
18
- rescue => e
19
- "āŒ Benchmark failed: #{e.message}"
20
- end
16
+ format_benchmark_result(code, iterations, total_ms, per_iteration_us)
17
+ rescue => e
18
+ "āŒ Benchmark failed: #{e.message}"
21
19
  end
22
20
 
23
21
  # Compare two code snippets
24
- def self.compare(code1, code2, iterations = 1000)
25
- begin
26
- time1 = Benchmark.measure do
27
- iterations.times { eval(code1) }
28
- end
29
-
30
- time2 = Benchmark.measure do
31
- iterations.times { eval(code2) }
32
- end
22
+ def self.compare(code1, code2, iterations = 1000, binding = nil)
23
+ time1 = Benchmark.measure do
24
+ iterations.times { binding ? eval(code1, binding) : eval(code1) }
25
+ end
33
26
 
34
- format_comparison(code1, code2, time1, time2, iterations)
35
- rescue => e
36
- "āŒ Comparison failed: #{e.message}"
27
+ time2 = Benchmark.measure do
28
+ iterations.times { binding ? eval(code2, binding) : eval(code2) }
37
29
  end
30
+
31
+ format_comparison(code1, code2, time1, time2, iterations)
32
+ rescue => e
33
+ "āŒ Comparison failed: #{e.message}"
38
34
  end
39
35
 
40
36
  # Profile memory
41
- def self.profile_memory(code, iterations = 100)
42
- begin
43
- gc_stat_before = GC.stat
44
-
45
- iterations.times { eval(code) }
46
-
47
- gc_stat_after = GC.stat
48
-
49
- format_memory_profile(gc_stat_before, gc_stat_after, iterations)
50
- rescue => e
51
- "āŒ Memory profile failed: #{e.message}"
52
- end
37
+ def self.profile_memory(code, iterations = 100, binding = nil)
38
+ gc_stat_before = GC.stat
39
+
40
+ iterations.times { binding ? eval(code, binding) : eval(code) }
41
+
42
+ gc_stat_after = GC.stat
43
+
44
+ format_memory_profile(gc_stat_before, gc_stat_after, iterations)
45
+ rescue => e
46
+ "āŒ Memory profile failed: #{e.message}"
53
47
  end
54
48
 
55
49
  private
56
50
 
51
+ def self.truncate(str, max)
52
+ str.length > max ? str[0, max - 3] + "..." : str
53
+ end
54
+
57
55
  def self.format_benchmark_result(code, iterations, total_ms, per_iteration_us)
58
56
  output = "ā±ļø Benchmark Results:\n\n"
59
- output += "Code: #{code.truncate(50)}\n"
57
+ output += "Code: #{truncate(code, 50)}\n"
60
58
  output += "Iterations: #{iterations}\n\n"
61
59
  output += "Total Time: #{total_ms.round(2)} ms\n"
62
60
  output += "Per Iteration: #{per_iteration_us.round(4)} µs\n"
63
- output += "Rate: #{(iterations / (total_ms / 1000)).round(0)} ops/sec\n"
61
+ output += "Rate: #{(iterations / (total_ms / 1000.0)).round(0)} ops/sec\n"
64
62
  output
65
63
  end
66
64
 
67
65
  def self.format_comparison(code1, code2, time1, time2, iterations)
68
- ratio = time2.real / time1.real
69
- faster = ratio < 1 ? "Code 2 is #{(1/ratio - 1) * 100}% faster" : "Code 1 is #{(ratio - 1) * 100}% faster"
66
+ ratio = time1.real > 0 ? time2.real / time1.real : 1.0
67
+ faster = if ratio < 1
68
+ "Code 2 is #{((1.0 / ratio - 1) * 100).round(1)}% faster"
69
+ else
70
+ "Code 1 is #{((ratio - 1) * 100).round(1)}% faster"
71
+ end
70
72
 
71
73
  output = "⚔ Comparison Results:\n\n"
72
74
  output += "Iterations: #{iterations}\n\n"
73
- output += "Code 1 (#{code1.truncate(40)}):\n"
75
+ output += "Code 1 (#{truncate(code1, 40)}):\n"
74
76
  output += " Time: #{(time1.real * 1000).round(2)} ms\n\n"
75
- output += "Code 2 (#{code2.truncate(40)}):\n"
77
+ output += "Code 2 (#{truncate(code2, 40)}):\n"
76
78
  output += " Time: #{(time2.real * 1000).round(2)} ms\n\n"
77
79
  output += "Winner: #{faster}\n"
78
80
  output
@@ -81,13 +83,14 @@ module PrettyIRB
81
83
  def self.format_memory_profile(before, after, iterations)
82
84
  allocated = after[:total_allocated_objects] - before[:total_allocated_objects]
83
85
  freed = after[:total_freed_objects] - before[:total_freed_objects]
86
+ net = allocated - freed
84
87
 
85
88
  output = "šŸ’¾ Memory Profile:\n\n"
86
89
  output += "Iterations: #{iterations}\n\n"
87
90
  output += "Objects Allocated: #{allocated}\n"
88
91
  output += "Objects Freed: #{freed}\n"
89
- output += "Net Objects: #{allocated - freed}\n"
90
- output += "Per Iteration: #{((allocated - freed) / iterations).round(1)} objects\n"
92
+ output += "Net Objects: #{net}\n"
93
+ output += "Per Iteration: #{(net.to_f / iterations).round(2)} objects\n"
91
94
  output
92
95
  end
93
96
  end
@@ -0,0 +1,40 @@
1
+ module PrettyIRB
2
+ # Global and per-session configuration options for PrettyIRB.
3
+ class Configuration
4
+ attr_accessor :colorize, :syntax_highlight, :auto_correct, :banner, :rails_env_prompt
5
+
6
+ def initialize
7
+ @colorize = true
8
+ @syntax_highlight = true
9
+ @auto_correct = true
10
+ @banner = true
11
+ @rails_env_prompt = true
12
+ end
13
+
14
+ # Return a duplicate configuration with any non-nil overrides applied.
15
+ def merge(overrides = {})
16
+ copy = dup
17
+ return copy if overrides.nil?
18
+
19
+ hash = overrides.respond_to?(:to_h) ? overrides.to_h : {}
20
+ hash.each do |key, value|
21
+ next if value.nil?
22
+
23
+ setter = "#{key}="
24
+ copy.public_send(setter, value) if copy.respond_to?(setter)
25
+ end
26
+ copy
27
+ end
28
+
29
+ def to_h
30
+ {
31
+ colorize: @colorize,
32
+ syntax_highlight: @syntax_highlight,
33
+ auto_correct: @auto_correct,
34
+ banner: @banner,
35
+ rails_env_prompt: @rails_env_prompt
36
+ }
37
+ end
38
+ end
39
+ end
40
+
@@ -1,66 +1,201 @@
1
- require "colorize"
2
-
3
- module PrettyIRB
4
- class Formatter
5
- class << self
6
- # Pretty print output with colors
7
- def format_output(output)
8
- case output
9
- when String
10
- output.light_blue
11
- when Numeric
12
- output.to_s.light_green
13
- when TrueClass, FalseClass
14
- output.to_s.light_cyan
15
- when NilClass
16
- "nil".light_black
17
- when Array
18
- format_array(output)
19
- when Hash
20
- format_hash(output)
21
- else
22
- output.inspect.light_yellow
23
- end
24
- rescue
25
- output.to_s
26
- end
27
-
28
- # Format error messages with pretty colors
29
- def format_error(error_message)
30
- error_message.red.bold
31
- rescue
32
- error_message
33
- end
34
-
35
- # Format success messages
36
- def format_success(message)
37
- message.green
38
- rescue
39
- message
40
- end
41
-
42
- # Format warning messages
43
- def format_warning(message)
44
- message.yellow
45
- rescue
46
- message
47
- end
48
-
49
- private
50
-
51
- def format_array(array)
52
- formatted = array.map { |item| format_output(item) }.join(", ")
53
- "[#{formatted}]".light_blue
54
- rescue
55
- array.inspect
56
- end
57
-
58
- def format_hash(hash)
59
- formatted = hash.map { |k, v| "#{format_output(k)}=>#{format_output(v)}" }.join(", ")
60
- "{#{formatted}}".light_blue
61
- rescue
62
- hash.inspect
63
- end
64
- end
65
- end
66
- end
1
+ require "colorize"
2
+ require "rouge"
3
+
4
+ module PrettyIRB
5
+ class Formatter
6
+ INLINE_MAX_WIDTH = 72
7
+ MAX_COLLECTION_ITEMS = 30
8
+
9
+ class << self
10
+ # Muted "=> " indicator so the returned value stands out clearly.
11
+ def format_result(output)
12
+ color("=> ", :light_black) + format_output(output)
13
+ rescue
14
+ "=> #{output}"
15
+ end
16
+
17
+ def format_output(value, indent: 0)
18
+ case value
19
+ when String then color(value.inspect, :light_yellow)
20
+ when Integer, Float then color(value.to_s, :light_green)
21
+ when Numeric then color(value.to_s, :light_green)
22
+ when TrueClass then color("true", :cyan)
23
+ when FalseClass then color("false", :cyan)
24
+ when NilClass then color("nil", :light_black)
25
+ when Symbol then color(value.inspect, :light_magenta)
26
+ when Regexp then color(value.inspect, :light_red)
27
+ when Range then color(value.inspect, :light_cyan)
28
+ when Class, Module then color(value.inspect, :light_blue)
29
+ when Time then color(value.inspect, :light_cyan)
30
+ when Array then format_array(value, indent: indent)
31
+ when Hash then format_hash(value, indent: indent)
32
+ else format_object(value, indent: indent)
33
+ end
34
+ rescue
35
+ value.to_s
36
+ end
37
+
38
+ # Rouge Monokai syntax highlight for Ruby code (used in history / snippets)
39
+ def highlight_code(code)
40
+ return code unless syntax_highlight? && colorize?
41
+
42
+ formatter = Rouge::Formatters::Terminal256.new(Rouge::Themes::Monokai.new)
43
+ lexer = Rouge::Lexers::Ruby.new
44
+ formatter.format(lexer.lex(code))
45
+ rescue
46
+ code
47
+ end
48
+
49
+ # "ErrorType: message" — bold red type, crisp light-red detail
50
+ def format_error(type, message = nil)
51
+ if colorize?
52
+ message ? type.red.bold + ": ".red + message.light_red : type.red.bold
53
+ else
54
+ message ? "#{type}: #{message}" : type.to_s
55
+ end
56
+ rescue
57
+ "#{type}: #{message}"
58
+ end
59
+
60
+ def format_success(message)
61
+ color(message, :green)
62
+ rescue
63
+ message
64
+ end
65
+
66
+ def format_warning(message)
67
+ color(message, :yellow)
68
+ rescue
69
+ message
70
+ end
71
+
72
+ private
73
+
74
+ def colorize?
75
+ return true unless defined?(PrettyIRB) && PrettyIRB.respond_to?(:configuration)
76
+
77
+ PrettyIRB.configuration.colorize
78
+ end
79
+
80
+ def syntax_highlight?
81
+ return true unless defined?(PrettyIRB) && PrettyIRB.respond_to?(:configuration)
82
+
83
+ PrettyIRB.configuration.syntax_highlight
84
+ end
85
+
86
+ def color(text, style)
87
+ colorize? ? text.public_send(style) : text
88
+ end
89
+
90
+ def visible_length(text)
91
+ defined?(PrettyIRB::UI) ? PrettyIRB::UI.visible_length(text) : text.gsub(/\e\[[0-9;]*m/, "").length
92
+ end
93
+
94
+ def format_object(value, indent: 0)
95
+ return color(value.inspect, :light_cyan) if defined?(Date) && value.is_a?(Date)
96
+ return format_active_record_relation(value, indent: indent) if active_record_relation?(value)
97
+ return format_active_record_model(value, indent: indent) if active_record_model?(value)
98
+ return format_struct(value, indent: indent) if value.is_a?(Struct)
99
+
100
+ color(value.inspect, :white)
101
+ end
102
+
103
+ def active_record_relation?(value)
104
+ defined?(ActiveRecord::Relation) && value.is_a?(ActiveRecord::Relation)
105
+ end
106
+
107
+ def active_record_model?(value)
108
+ defined?(ActiveRecord::Base) && value.is_a?(ActiveRecord::Base) && value.respond_to?(:attributes)
109
+ end
110
+
111
+ def format_active_record_model(record, indent: 0)
112
+ attrs = record.attributes
113
+ klass = record.class.name || "ActiveRecord"
114
+ new_tag = record.respond_to?(:new_record?) && record.new_record? ? color(" (new)", :yellow) : ""
115
+ header = color("#<#{klass}", :light_cyan) + new_tag
116
+ closing = color(">", :light_cyan)
117
+
118
+ return "#{header} #{color('(no attributes)', :light_black)}#{closing}" if attrs.nil? || attrs.empty?
119
+
120
+ pairs = attrs.map { |k, v| color("#{k}: ", :light_black) + format_output(v, indent: indent + 1) }
121
+ join_collection(pairs, "#{header} ", closing, indent: indent, inline_limit: 3, inline_separator: ", ")
122
+ rescue
123
+ color(record.inspect, :white)
124
+ end
125
+
126
+ def format_active_record_relation(relation, indent: 0)
127
+ loaded = relation.respond_to?(:loaded?) && relation.loaded?
128
+ if loaded
129
+ all_records = relation.to_a
130
+ truncated = all_records.length > MAX_COLLECTION_ITEMS
131
+ shown = all_records.first(MAX_COLLECTION_ITEMS)
132
+ trunc_label = "… (#{all_records.length} total)"
133
+ else
134
+ fetched = relation.limit(MAX_COLLECTION_ITEMS + 1).to_a
135
+ truncated = fetched.length > MAX_COLLECTION_ITEMS
136
+ shown = fetched.first(MAX_COLLECTION_ITEMS)
137
+ trunc_label = "… (showing first #{MAX_COLLECTION_ITEMS})"
138
+ end
139
+
140
+ header = color("#<ActiveRecord::Relation [", :light_cyan)
141
+ closing = color("]>", :light_cyan)
142
+ return "#{header}#{closing}" if shown.empty?
143
+
144
+ items = shown.map { |rec| format_output(rec, indent: indent + 1) }
145
+ items << color(trunc_label, :light_black) if truncated
146
+
147
+ join_collection(items, header, closing, indent: indent)
148
+ rescue
149
+ color(relation.inspect, :white)
150
+ end
151
+
152
+ def format_struct(struct_obj, indent: 0)
153
+ name = struct_obj.class.name || "Struct"
154
+ pairs = struct_obj.to_h.map do |k, v|
155
+ color("#{k}: ", :light_black) + format_output(v, indent: indent + 1)
156
+ end
157
+ header = color("#<struct #{name}", :light_cyan)
158
+ closing = color(">", :light_cyan)
159
+ join_collection(pairs, "#{header} ", closing, indent: indent, inline_limit: 4)
160
+ rescue
161
+ color(struct_obj.inspect, :white)
162
+ end
163
+
164
+ def format_array(arr, indent: 0)
165
+ return color("[]", :light_black) if arr.empty?
166
+
167
+ items = arr.first(MAX_COLLECTION_ITEMS).map { |v| format_output(v, indent: indent + 1) }
168
+ items << color("… (#{arr.length} total)", :light_black) if arr.length > MAX_COLLECTION_ITEMS
169
+
170
+ join_collection(items, color("[", :light_black), color("]", :light_black), indent: indent, inline_limit: 6)
171
+ rescue
172
+ arr.inspect
173
+ end
174
+
175
+ def format_hash(hash, indent: 0)
176
+ return color("{}", :light_black) if hash.empty?
177
+
178
+ pairs = hash.first(MAX_COLLECTION_ITEMS).map do |k, v|
179
+ format_output(k, indent: indent + 1) + color(" => ", :light_black) + format_output(v, indent: indent + 1)
180
+ end
181
+ pairs << color("… (#{hash.length} total)", :light_black) if hash.length > MAX_COLLECTION_ITEMS
182
+
183
+ join_collection(pairs, color("{", :light_black), color("}", :light_black), indent: indent, inline_limit: 4)
184
+ rescue
185
+ hash.inspect
186
+ end
187
+
188
+ def join_collection(entries, open_delim, close_delim, indent:, inline_limit: nil, inline_separator: ", ")
189
+ inline = "#{open_delim}#{entries.join(color(inline_separator, :light_black))}#{close_delim}"
190
+ if inline_limit && entries.length <= inline_limit && !inline.include?("\n") && visible_length(inline) <= INLINE_MAX_WIDTH
191
+ return inline
192
+ end
193
+
194
+ pad_inner = " " * (indent + 1)
195
+ pad_outer = " " * indent
196
+ body = entries.map { |entry| "#{pad_inner}#{entry}" }.join(color(",\n", :light_black))
197
+ "#{open_delim.rstrip}\n#{body}\n#{pad_outer}#{close_delim}"
198
+ end
199
+ end
200
+ end
201
+ end