pretty_irb 0.1.3 → 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: de7427c34f3650136e317c86f5b32d53a24c4a883c4f3c70dc07684653cab46b
4
- data.tar.gz: 258568ee7bc6cc9d42c20d56f7ad80ca16f0a7c63b3b303f0823f405ac67cfaa
3
+ metadata.gz: 95ec3de4041e5f89096a2fe2f3a2c34506bde577008b8539d6d5459067d0b360
4
+ data.tar.gz: 2277edc3c5e7913d3537b219e77bc0f0fd85d33c47a259bcc50a57f84f542296
5
5
  SHA512:
6
- metadata.gz: 3cc973dea626a8ee900e7fbbf26b62d4a941a00fb0f95b137bad0d388c0989e6cfec92d33a35d707f37ddfaac64c24dbe2b5ab8b31ab2b5c48d6c14324368a2e
7
- data.tar.gz: 02f2fcd034a86279c301c20497d0fdba10672e528a3df3026c1b6ac173beb13199a0332341ef67f141c92c034bfc23ba5622376f9db61ba8fd2ccca9bd38ac94
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
@@ -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
+
@@ -3,27 +3,33 @@ require "rouge"
3
3
 
4
4
  module PrettyIRB
5
5
  class Formatter
6
+ INLINE_MAX_WIDTH = 72
7
+ MAX_COLLECTION_ITEMS = 30
8
+
6
9
  class << self
7
- # "=> value" — identical to how IRB shows results
10
+ # Muted "=> " indicator so the returned value stands out clearly.
8
11
  def format_result(output)
9
- "=> ".white + format_output(output)
12
+ color("=> ", :light_black) + format_output(output)
10
13
  rescue
11
14
  "=> #{output}"
12
15
  end
13
16
 
14
- def format_output(value)
17
+ def format_output(value, indent: 0)
15
18
  case value
16
- when String then value.inspect.light_yellow
17
- when Integer then value.to_s.light_green
18
- when Float then value.to_s.light_green
19
- when Numeric then value.to_s.light_green
20
- when TrueClass then "true".cyan
21
- when FalseClass then "false".cyan
22
- when NilClass then "nil".light_black
23
- when Symbol then value.inspect.light_magenta
24
- when Array then format_array(value)
25
- when Hash then format_hash(value)
26
- else value.inspect.white
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)
27
33
  end
28
34
  rescue
29
35
  value.to_s
@@ -31,6 +37,8 @@ module PrettyIRB
31
37
 
32
38
  # Rouge Monokai syntax highlight for Ruby code (used in history / snippets)
33
39
  def highlight_code(code)
40
+ return code unless syntax_highlight? && colorize?
41
+
34
42
  formatter = Rouge::Formatters::Terminal256.new(Rouge::Themes::Monokai.new)
35
43
  lexer = Rouge::Lexers::Ruby.new
36
44
  formatter.format(lexer.lex(code))
@@ -38,52 +46,156 @@ module PrettyIRB
38
46
  code
39
47
  end
40
48
 
41
- # "ErrorType: message" — red type, dim message
49
+ # "ErrorType: message" — bold red type, crisp light-red detail
42
50
  def format_error(type, message = nil)
43
- if message
44
- type.red.bold + ": ".red + message.light_red
51
+ if colorize?
52
+ message ? type.red.bold + ": ".red + message.light_red : type.red.bold
45
53
  else
46
- type.red.bold
54
+ message ? "#{type}: #{message}" : type.to_s
47
55
  end
48
56
  rescue
49
57
  "#{type}: #{message}"
50
58
  end
51
59
 
52
60
  def format_success(message)
53
- message.green
61
+ color(message, :green)
54
62
  rescue
55
63
  message
56
64
  end
57
65
 
58
66
  def format_warning(message)
59
- message.yellow
67
+ color(message, :yellow)
60
68
  rescue
61
69
  message
62
70
  end
63
71
 
64
72
  private
65
73
 
66
- def format_array(arr)
67
- return "[]".white if arr.empty?
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?
68
143
 
69
- items = arr.first(30).map { |v| format_output(v) }
70
- items << "… (#{arr.length} total)".light_black if arr.length > 30
71
- "[".white + items.join(", ".white) + "]".white
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)
72
171
  rescue
73
172
  arr.inspect
74
173
  end
75
174
 
76
- def format_hash(hash)
77
- return "{}".white if hash.empty?
175
+ def format_hash(hash, indent: 0)
176
+ return color("{}", :light_black) if hash.empty?
78
177
 
79
- pairs = hash.first(30).map do |k, v|
80
- format_output(k) + " => ".white + format_output(v)
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)
81
180
  end
82
- pairs << "… (#{hash.length} total)".light_black if hash.length > 30
83
- "{".white + pairs.join(", ".white) + "}".white
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)
84
184
  rescue
85
185
  hash.inspect
86
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
87
199
  end
88
200
  end
89
201
  end
@@ -19,6 +19,13 @@ module PrettyIRB
19
19
  }
20
20
  end
21
21
 
22
+ # Attach an error message to the most recent history entry
23
+ def record_error(error_message)
24
+ return if @entries.empty?
25
+
26
+ @entries.last[:error] = error_message
27
+ end
28
+
22
29
  # Search history by keyword
23
30
  def search(keyword)
24
31
  results = @entries.select { |entry| entry[:code].include?(keyword) }
@@ -51,8 +58,13 @@ module PrettyIRB
51
58
  # Export history to file
52
59
  def export(filename)
53
60
  path = File.expand_path(filename.to_s)
54
- # Refuse paths that point at system-sensitive locations
55
- if path.start_with?("/etc", "/sys", "/proc", "/dev")
61
+ # Refuse paths that point at system-sensitive locations (Unix and Windows)
62
+ normalized = path.tr('\\', '/')
63
+ unix_sensitive = %w[/etc /sys /proc /dev]
64
+ windows_sensitive = %w[C:/Windows C:/Program]
65
+ blocked = unix_sensitive.any? { |p| normalized.start_with?(p) } ||
66
+ windows_sensitive.any? { |p| normalized.start_with?(p) }
67
+ if blocked
56
68
  return "āŒ Refusing to write to system path: #{path}"
57
69
  end
58
70
  File.write(path, format_for_export)