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.
@@ -1,387 +1,435 @@
1
- require "irb"
2
- require "reline"
3
-
4
- module PrettyIRB
5
- class Shell
6
- BANNER = <<~BANNER
7
- ╔═══════════════════════════════════════════════════════════╗
8
- ║ 🎨 Welcome to Pretty IRB 🎨 ║
9
- ║ ║
10
- ║ Enhanced Interactive Ruby Shell with Auto-Correct ║
11
- ║ Type 'help' for commands or 'exit' to quit ║
12
- ╚═══════════════════════════════════════════════════════════╝
13
- BANNER
14
-
15
- def initialize(config = {})
16
- @config = config
17
- @history_manager = HistoryManager.new
18
- @snippet_manager = SnippetManager.new
19
- @variable_inspector = VariableInspector.new(TOPLEVEL_BINDING)
20
- @input_buffer = ""
21
- @binding = TOPLEVEL_BINDING
22
- end
23
-
24
- def run
25
- puts BANNER.light_magenta
26
- start_repl
27
- end
28
-
29
- private
30
-
31
- def start_repl
32
- loop do
33
- begin
34
- input = read_input
35
- break if input.nil? || input.downcase == "exit"
36
-
37
- if input.downcase == "help"
38
- show_help
39
- next
40
- end
41
-
42
- # Handle special commands
43
- if input.downcase == "clear"
44
- system("clear") || system("cls")
45
- next
46
- end
47
-
48
- # Handle history commands
49
- if input.start_with?("history")
50
- handle_history_command(input)
51
- next
52
- end
53
-
54
- # Handle snippet commands
55
- if input.start_with?("snippet")
56
- handle_snippet_command(input)
57
- next
58
- end
59
-
60
- # Handle variable commands
61
- if input.start_with?("vars")
62
- handle_variable_command(input)
63
- next
64
- end
65
-
66
- # Handle benchmark commands
67
- if input.start_with?("benchmark") || input.start_with?("bench")
68
- handle_benchmark_command(input)
69
- next
70
- end
71
-
72
- # Handle cheatsheet commands
73
- if input.start_with?("cheat")
74
- handle_cheatsheet_command(input)
75
- next
76
- end
77
-
78
- # Handle AI help commands
79
- if input.start_with?("?")
80
- handle_ai_command(input)
81
- next
82
- end
83
-
84
- execute_code(input)
85
- @history_manager.add(input)
86
-
87
- rescue Interrupt
88
- puts "\n" + Formatter.format_warning("Interrupted. Type 'exit' to quit.")
89
- rescue EOFError
90
- puts "\n" + Formatter.format_success("Goodbye!")
91
- break
92
- rescue StandardError => e
93
- handle_error(e)
94
- end
95
- end
96
- end
97
-
98
- def read_input
99
- prompt = "pretty_irb".light_magenta + " >> ".light_magenta
100
- input = Reline.readline(prompt, true)
101
- return nil if input.nil?
102
-
103
- # If the line appears to start a block or otherwise is incomplete,
104
- # collect additional lines until the code is complete.
105
- if incomplete_code?(input)
106
- buffer = input + "\n"
107
- while (more = Reline.readline("... ".light_magenta, true))
108
- break if more.nil?
109
- buffer += more + "\n"
110
- break unless incomplete_code?(buffer)
111
- end
112
-
113
- return buffer
114
- end
115
-
116
- input
117
- end
118
-
119
- # Heuristic to detect incomplete Ruby code.
120
- # Returns true if there are unclosed block tokens, brackets, or quotes.
121
- def incomplete_code?(code)
122
- return true if code.nil? || code.strip.empty?
123
-
124
- # Count block starters and 'end'
125
- opens = 0
126
- opens += code.scan(/\b(do|def|class|module|if|case|begin|for|while|until|unless)\b/).size
127
- closes = code.scan(/\bend\b/).size
128
- return true if opens > closes
129
-
130
- # Brackets / braces / parentheses
131
- return true if code.count('(') > code.count(')')
132
- return true if code.count('[') > code.count(']')
133
- return true if code.count('{') > code.count('}')
134
-
135
- # Unclosed quotes (simple heuristic, ignores escaped quotes)
136
- single_quotes = code.scan(/(?<!\\)'/).size
137
- double_quotes = code.scan(/(?<!\\)"/).size
138
- return true if single_quotes.odd? || double_quotes.odd?
139
-
140
- # Line continuation with trailing backslash
141
- return true if code.rstrip.end_with?('\\')
142
-
143
- false
144
- end
145
-
146
- def execute_code(code)
147
- # Auto-correct the code
148
- corrected_code = AutoCorrector.auto_correct_code(code)
149
-
150
- # Show if code was corrected
151
- if corrected_code != code
152
- puts Formatter.format_warning("↻ Auto-corrected: #{corrected_code}")
153
- end
154
-
155
- begin
156
- # Evaluate the code
157
- result = eval(corrected_code, @binding)
158
-
159
- # Display the result
160
- unless result.nil?
161
- puts Formatter.format_output(result)
162
- end
163
-
164
- rescue NameError => e
165
- handle_name_error(e)
166
- rescue NoMethodError => e
167
- handle_method_error(e)
168
- rescue SyntaxError => e
169
- handle_syntax_error(e)
170
- rescue StandardError => e
171
- handle_error(e)
172
- end
173
- end
174
-
175
- def handle_name_error(error)
176
- puts Formatter.format_error("NameError: #{error.message}")
177
- suggestions = AutoCorrector.suggest_corrections(error)
178
- puts suggestions if suggestions && !suggestions.empty?
179
- end
180
-
181
- def handle_method_error(error)
182
- puts Formatter.format_error("NoMethodError: #{error.message}")
183
- suggestions = AutoCorrector.suggest_corrections(error)
184
- puts suggestions if suggestions && !suggestions.empty?
185
- end
186
-
187
- def handle_syntax_error(error)
188
- puts Formatter.format_error("SyntaxError: #{error.message}")
189
- suggestions = AutoCorrector.suggest_corrections(error)
190
- puts suggestions if suggestions && !suggestions.empty?
191
- end
192
-
193
- def handle_error(error)
194
- puts Formatter.format_error("#{error.class}: #{error.message}")
195
- if error.backtrace
196
- puts Formatter.format_warning("Backtrace:")
197
- error.backtrace.first(5).each do |line|
198
- puts " #{line}"
199
- end
200
- end
201
- end
202
-
203
- def show_help
204
- help_text = <<~HELP
205
- #{'=== Pretty IRB Commands ==='.light_magenta}
206
-
207
- exit, quit Exit the shell
208
- help Show this help message
209
- clear Clear the screen
210
-
211
- #{'=== History Commands ==='.light_magenta}
212
-
213
- history Show all command history
214
- history search KEYWORD Search history
215
- history last N Show last N commands
216
- history export FILE Export to file
217
- history clear Clear history
218
-
219
- #{'=== Snippet Commands ==='.light_magenta}
220
-
221
- snippet list List all snippets
222
- snippet save NAME CODE Save code snippet
223
- snippet load NAME Load and execute snippet
224
- snippet show NAME Show snippet code
225
- snippet delete NAME Delete snippet
226
- snippet search KEYWORD Search snippets
227
-
228
- #{'=== Variable Commands ==='.light_magenta}
229
-
230
- vars List all variables
231
- vars VARNAME Inspect variable
232
- vars type:TYPE Variables of type
233
- vars search:KEYWORD Search variables
234
- vars memory Memory usage
235
-
236
- #{'=== Benchmark Commands ==='.light_magenta}
237
-
238
- bench CODE Benchmark code
239
- bench compare CODE1 vs CODE2 Compare two snippets
240
- bench memory CODE Profile memory
241
-
242
- #{'=== Cheatsheet Commands ==='.light_magenta}
243
-
244
- cheat Show Ruby cheatsheet
245
- cheat TOPIC Cheatsheet for topic
246
- (array, hash, string, enumerable, file, regex, date)
247
-
248
- #{'=== AI Help Commands ==='.light_magenta}
249
-
250
- ?explain(method) Explain a Ruby method
251
- ?example(topic) Get code examples
252
- ?debug(code) Analyze code for issues
253
- ?practices(topic) Learn best practices
254
- ?ref(keyword) Quick reference
255
-
256
- #{'You can type any valid Ruby code and it will be executed!'.light_green}
257
- HELP
258
- puts help_text
259
- end
260
-
261
- def handle_history_command(input)
262
- case input.downcase
263
- when "history"
264
- puts @history_manager.all
265
- when /^history\s+search\s+(.+)$/
266
- keyword = $1.strip
267
- puts @history_manager.search(keyword)
268
- when /^history\s+last\s+(\d+)$/
269
- n = $1.to_i
270
- puts @history_manager.last(n)
271
- when /^history\s+export\s+(.+)$/
272
- filename = $1.strip
273
- puts @history_manager.export(filename)
274
- when "history clear"
275
- puts @history_manager.clear
276
- else
277
- puts Formatter.format_warning("Usage: history | history search KEYWORD | history last N | history export FILE | history clear")
278
- end
279
- end
280
-
281
- def handle_snippet_command(input)
282
- case input.downcase
283
- when "snippet list"
284
- puts @snippet_manager.list
285
- when /^snippet\s+save\s+(\w+)\s+(.+)$/
286
- name = $1
287
- code = $2
288
- puts @snippet_manager.save(name, code)
289
- when /^snippet\s+load\s+(\w+)$/
290
- name = $1
291
- code = @snippet_manager.load(name)
292
- if code
293
- puts Formatter.format_success("📌 Loaded snippet: #{name}")
294
- puts code
295
- execute_code(code)
296
- else
297
- puts code
298
- end
299
- when /^snippet\s+show\s+(\w+)$/
300
- name = $1
301
- puts @snippet_manager.show(name)
302
- when /^snippet\s+delete\s+(\w+)$/
303
- name = $1
304
- puts @snippet_manager.delete(name)
305
- when /^snippet\s+search\s+(.+)$/
306
- keyword = $1.strip
307
- puts @snippet_manager.search(keyword)
308
- else
309
- puts Formatter.format_warning("Usage: snippet list | snippet save NAME CODE | snippet load NAME | snippet show NAME | snippet delete NAME | snippet search KEYWORD")
310
- end
311
- end
312
-
313
- def handle_variable_command(input)
314
- case input.downcase
315
- when "vars"
316
- puts @variable_inspector.list_variables
317
- when /^vars\s+(.+)$/
318
- var_name = $1.strip
319
- if var_name == "memory"
320
- puts @variable_inspector.memory_usage
321
- elsif var_name.start_with?("type:")
322
- type = var_name.sub("type:", "").strip
323
- puts @variable_inspector.by_type(type)
324
- elsif var_name.start_with?("search:")
325
- keyword = var_name.sub("search:", "").strip
326
- puts @variable_inspector.search(keyword)
327
- else
328
- puts @variable_inspector.inspect_var(var_name)
329
- end
330
- else
331
- puts Formatter.format_warning("Usage: vars | vars VARNAME | vars type:TYPE | vars search:KEYWORD | vars memory")
332
- end
333
- end
334
-
335
- def handle_benchmark_command(input)
336
- case input
337
- when /^bench(?:mark)?\s+(.+)$/
338
- code = $1
339
- puts Benchmarker.benchmark(code)
340
- when /^bench(?:mark)?\s+compare\s+(.+?)\s+vs\s+(.+)$/
341
- code1 = $1
342
- code2 = $2
343
- puts Benchmarker.compare(code1, code2)
344
- when /^bench(?:mark)?\s+memory\s+(.+)$/
345
- code = $1
346
- puts Benchmarker.profile_memory(code)
347
- else
348
- puts Formatter.format_warning("Usage: bench CODE | bench compare CODE1 vs CODE2 | bench memory CODE")
349
- end
350
- end
351
-
352
- def handle_cheatsheet_command(input)
353
- case input.downcase
354
- when "cheat"
355
- puts CheatSheet.show
356
- when /^cheat\s+(\w+)$/
357
- topic = $1
358
- puts CheatSheet.show(topic)
359
- else
360
- puts Formatter.format_warning("Usage: cheat | cheat TOPIC")
361
- puts "Topics: array, hash, string, enumerable, file, regex, date"
362
- end
363
- end
364
-
365
- def handle_ai_command(input)
366
- case input
367
- when /^\?explain\((.*?)\)$/
368
- method_name = $1.gsub(/'|"/, "")
369
- puts AIHelper.explain_method(method_name)
370
- when /^\?example\((.*?)\)$/
371
- topic = $1.gsub(/'|"/, "")
372
- puts AIHelper.get_example(topic)
373
- when /^\?debug\((.*)\)$/
374
- code_snippet = $1
375
- puts AIHelper.debug_code(code_snippet)
376
- when /^\?practices\((.*?)\)$/
377
- topic = $1.gsub(/'|"/, "")
378
- puts AIHelper.best_practices(topic)
379
- when /^\?ref\((.*?)\)$/
380
- keyword = $1.gsub(/'|"/, "")
381
- puts AIHelper.quick_reference(keyword)
382
- else
383
- puts Formatter.format_warning("❓ Unknown AI command. Type 'help' for available commands.")
384
- end
385
- end
386
- end
387
- end
1
+ require "irb"
2
+
3
+ module PrettyIRB
4
+ class Shell
5
+ BANNER = <<~BANNER
6
+ ╭─────────────────────────────────────────────────────────────╮
7
+ │ pretty_irb v#{PrettyIRB::VERSION} · Enhanced Interactive Ruby Shell │
8
+ │ type commands to list extras · exit / Ctrl+D to quit │
9
+ ╰─────────────────────────────────────────────────────────────╯
10
+ BANNER
11
+
12
+ module OutputHook
13
+ def output_value(omit = false)
14
+ return super unless PrettyIRB::Shell.active?
15
+ return if omit
16
+
17
+ puts PrettyIRB::Formatter.format_result(@context.last_value)
18
+ end
19
+ end
20
+
21
+ module ExceptionHook
22
+ def handle_exception(exc)
23
+ return super unless PrettyIRB::Shell.active?
24
+
25
+ first_line = exc.message.to_s.lines.first&.strip
26
+ PrettyIRB::Shell.history_manager&.record_error("#{exc.class}: #{first_line}")
27
+ super
28
+ PrettyIRB::Shell.print_exception_hint(exc)
29
+ end
30
+ end
31
+
32
+ module EvalHook
33
+ def evaluate(code, line_no, *args, **kwargs, &block)
34
+ raw = nil
35
+ if PrettyIRB::Shell.active?
36
+ raw = PrettyIRB::Shell.extract_raw_code(code)
37
+ if raw && PrettyIRB.configuration.auto_correct
38
+ corrected = PrettyIRB::AutoCorrector.auto_correct_code(raw)
39
+ if corrected.strip != raw.strip
40
+ label = PrettyIRB::UI.paint(" ↳ auto-corrected: ", :light_black)
41
+ fixed = PrettyIRB::UI.paint(corrected.strip, :yellow)
42
+ warn "#{label}#{fixed}"
43
+ end
44
+ end
45
+ end
46
+
47
+ result = begin
48
+ if kwargs.empty?
49
+ super(code, line_no, *args, &block)
50
+ else
51
+ super(code, line_no, *args, **kwargs, &block)
52
+ end
53
+ rescue ArgumentError
54
+ super(code, line_no)
55
+ end
56
+
57
+ if PrettyIRB::Shell.active? && raw && !raw.strip.empty?
58
+ preview = begin
59
+ str = result.inspect
60
+ str.length > 80 ? "#{str[0, 77]}..." : str
61
+ rescue
62
+ nil
63
+ end
64
+ PrettyIRB::Shell.history_manager&.add(raw.strip, preview)
65
+ end
66
+
67
+ result
68
+ end
69
+ end
70
+
71
+ module RailsIRBInitHook
72
+ def initialize(*args, **kwargs, &block)
73
+ if (shell = PrettyIRB::Shell.consume_pending_rails_shell)
74
+ shell.apply_irb_configuration!
75
+ end
76
+
77
+ if kwargs.empty?
78
+ super(*args, &block)
79
+ else
80
+ super(*args, **kwargs, &block)
81
+ end
82
+ end
83
+ end
84
+
85
+ class << self
86
+ attr_accessor :history_manager, :snippet_manager, :active, :pending_rails_shell
87
+
88
+ def active?
89
+ !!@active
90
+ end
91
+
92
+ def consume_pending_rails_shell
93
+ shell = @pending_rails_shell
94
+ @pending_rails_shell = nil
95
+ shell
96
+ end
97
+
98
+ def extract_raw_code(code)
99
+ if code.is_a?(String)
100
+ code
101
+ elsif code.respond_to?(:code)
102
+ code.code.to_s
103
+ end
104
+ end
105
+
106
+ def print_exception_hint(exc)
107
+ return unless PrettyIRB.configuration.auto_correct
108
+
109
+ suggestion = PrettyIRB::AutoCorrector.suggest_corrections(exc).to_s.strip
110
+ return if suggestion.empty?
111
+ if exc.message.to_s.include?("Did you mean?") &&
112
+ !suggestion.include?("Receiver is nil") &&
113
+ !suggestion.include?("Did you mean `.")
114
+ return
115
+ end
116
+
117
+ puts " #{PrettyIRB::UI.paint(suggestion, :yellow)}"
118
+ rescue
119
+ nil
120
+ end
121
+ end
122
+
123
+ def initialize(config = {})
124
+ @config = config || {}
125
+ end
126
+
127
+ def run
128
+ STDOUT.sync = true
129
+ resolved_config = resolve_configuration
130
+ prev_config = PrettyIRB.configuration
131
+ PrettyIRB.instance_variable_set(:@configuration, resolved_config)
132
+ self.class.active = true
133
+
134
+ puts render_banner if resolved_config.banner
135
+
136
+ self.class.history_manager = HistoryManager.new
137
+ self.class.snippet_manager = SnippetManager.new
138
+
139
+ # Step 1: patch IRB classes idempotently before anything else
140
+ install_output_hook
141
+ install_eval_hook
142
+ install_exception_hook
143
+
144
+ @using_rails_irb_console = rails_irb_console_available?
145
+ if @using_rails_irb_console
146
+ run_with_rails_irb_console
147
+ else
148
+ run_standalone_or_legacy_rails
149
+ end
150
+ ensure
151
+ @using_rails_irb_console = false
152
+ self.class.consume_pending_rails_shell
153
+ self.class.active = false
154
+ PrettyIRB.instance_variable_set(:@configuration, prev_config)
155
+ end
156
+
157
+ # Applies PrettyIRB's Reline tuning, prompt, and IRB_RC hook onto the
158
+ # current IRB.conf after IRB.setup has initialized it.
159
+ def apply_irb_configuration!
160
+ tune_input_mode
161
+ configure_prompt
162
+ install_irb_rc_hook
163
+ end
164
+
165
+ private
166
+
167
+ def resolve_configuration
168
+ PrettyIRB.configuration.merge(@config)
169
+ end
170
+
171
+ def rails_app
172
+ return @config[:app] if @config.is_a?(Hash) && @config[:app]
173
+ return Rails.application if defined?(Rails) && Rails.respond_to?(:application)
174
+
175
+ nil
176
+ end
177
+
178
+ def rails_irb_console_available?
179
+ return false unless rails_app
180
+ return true if defined?(Rails::Console::IRBConsole)
181
+
182
+ require "rails/commands/console/irb_console"
183
+ !!defined?(Rails::Console::IRBConsole)
184
+ rescue LoadError
185
+ false
186
+ end
187
+
188
+ # Rails 7.2+ / 8.x delegates IRB initialization, helper registration
189
+ # (`reload!`, `app`, `helper`, `controller`), and backtrace cleaning to
190
+ # Rails::Console::IRBConsole. Hook into IRB::Irb#initialize so PrettyIRB
191
+ # layers on top right after IRB.setup(nil) completes.
192
+ def run_with_rails_irb_console
193
+ install_rails_irb_hook
194
+ self.class.pending_rails_shell = self
195
+ build_rails_irb_console(rails_app).start
196
+ end
197
+
198
+ def build_rails_irb_console(app)
199
+ init_arity = Rails::Console::IRBConsole.instance_method(:initialize).arity
200
+ if init_arity == 1
201
+ Rails::Console::IRBConsole.new(app)
202
+ else
203
+ Rails::Console::IRBConsole.new(app, @config[:rails_options] || {})
204
+ end
205
+ end
206
+
207
+ # Standalone Ruby or Rails 7.0 / 7.1 (before Rails::Console::IRBConsole).
208
+ def run_standalone_or_legacy_rails
209
+ app = rails_app
210
+ load_legacy_rails_console_helpers if app
211
+
212
+ # IRB.setup populates IRB.conf (and resets IRB_RC to nil).
213
+ IRB.setup(nil)
214
+
215
+ configure_legacy_rails_irb(app) if app
216
+ apply_irb_configuration!
217
+
218
+ irb = IRB::Irb.new
219
+ IRB.conf[:MAIN_CONTEXT] = irb.context
220
+ irb.run(IRB.conf)
221
+ end
222
+
223
+ def load_legacy_rails_console_helpers
224
+ require "rails/console/app"
225
+ require "rails/console/helpers"
226
+ rescue LoadError
227
+ nil
228
+ end
229
+
230
+ def configure_legacy_rails_irb(app)
231
+ IRB.conf[:IRB_NAME] = rails_app_name(app)
232
+ if !rails_local_env? && !ENV.key?("IRB_USE_AUTOCOMPLETE")
233
+ IRB.conf[:USE_AUTOCOMPLETE] = false
234
+ end
235
+
236
+ if defined?(Rails) && Rails.respond_to?(:backtrace_cleaner) && Rails.backtrace_cleaner
237
+ prev_filter = IRB.conf[:BACKTRACE_FILTER]
238
+ IRB.conf[:BACKTRACE_FILTER] = lambda do |backtrace|
239
+ backtrace = prev_filter.call(backtrace) if prev_filter.respond_to?(:call)
240
+ Rails.backtrace_cleaner.filter(backtrace)
241
+ end
242
+ end
243
+
244
+ if defined?(IRB::ExtendCommandBundle) && defined?(Rails::ConsoleMethods)
245
+ IRB::ExtendCommandBundle.include(Rails::ConsoleMethods)
246
+ end
247
+ end
248
+
249
+ def rails_app_name(app)
250
+ if app.respond_to?(:name) && app.name
251
+ app.name.to_s
252
+ elsif app.class.respond_to?(:module_parent_name) && app.class.module_parent_name
253
+ app.class.module_parent_name.downcase
254
+ else
255
+ "rails"
256
+ end
257
+ end
258
+
259
+ def rails_local_env?
260
+ return true unless defined?(Rails) && Rails.respond_to?(:env) && Rails.env
261
+
262
+ if Rails.env.respond_to?(:local?)
263
+ Rails.env.local?
264
+ else
265
+ Rails.env.development? || Rails.env.test?
266
+ end
267
+ end
268
+
269
+ # ── Terminal / input detection ────────────────────────────────────────────
270
+ # On Windows, Reline uses a Win32 API console handler. In "legacy
271
+ # console" mode (old conhost.exe — PowerShell or cmd.exe without
272
+ # Windows Terminal) Alt+key sequences are read in a way that blocks the
273
+ # input loop, freezing the entire REPL.
274
+
275
+ def tune_input_mode
276
+ return unless Gem.win_platform?
277
+
278
+ legacy = begin
279
+ Reline::IOGate.instance_variable_get(:@legacy_console)
280
+ rescue
281
+ false
282
+ end
283
+
284
+ if legacy
285
+ IRB.conf[:USE_MULTILINE] = false # avoid Reline Windows hang
286
+ IRB.conf[:USE_AUTOCOMPLETE] = false
287
+ msg = " ℹ Legacy Windows console detected — running in single-line mode.\n" \
288
+ " Open Windows Terminal for full multiline & autocomplete support."
289
+ puts style_text(msg, :light_black)
290
+ end
291
+ rescue
292
+ nil
293
+ end
294
+
295
+ # ── Banner & Prompt ───────────────────────────────────────────────────────
296
+
297
+ def render_banner
298
+ title = "#{style_text('pretty_irb', :white, :bold)} #{style_text("v#{PrettyIRB::VERSION}", :cyan)}"
299
+ lines = [
300
+ "#{style_text('Enhanced Interactive Ruby Shell', :white)} #{style_text("· Ruby #{RUBY_VERSION}", :light_black)}"
301
+ ]
302
+
303
+ if (rails_line = build_rails_banner_line)
304
+ lines << rails_line
305
+ end
306
+
307
+ lines << :divider
308
+ lines << [
309
+ "#{style_text('commands', :cyan)} #{style_text('extras', :light_black)}",
310
+ "#{style_text('vars', :cyan)} #{style_text('scope', :light_black)}",
311
+ "#{style_text('bench', :cyan)} #{style_text('profile', :light_black)}",
312
+ "#{style_text('help', :cyan)} #{style_text('IRB', :light_black)}",
313
+ "#{style_text('Ctrl+D', :white)} #{style_text('exit', :light_black)}"
314
+ ].join(style_text(" · ", :light_black))
315
+
316
+ PrettyIRB::UI.box(lines, title: title, min_width: 66)
317
+ end
318
+
319
+ def build_rails_banner_line
320
+ app = rails_app
321
+ return nil unless app
322
+
323
+ rails_ver = defined?(Rails) && Rails.respond_to?(:version) ? "Rails #{Rails.version}" : "Rails"
324
+ parts = [style_text("#{rails_app_name(app)} · #{rails_ver}", :light_black)]
325
+ if (badge = rails_env_badge)
326
+ parts << badge
327
+ end
328
+ parts << PrettyIRB::UI.badge("SANDBOX", color: :yellow, bold: true) if rails_sandbox?(app)
329
+ parts.join(" ")
330
+ end
331
+
332
+ def rails_sandbox?(app = rails_app)
333
+ app && app.respond_to?(:sandbox?) && app.sandbox?
334
+ end
335
+
336
+ def configure_prompt
337
+ prefix = build_prompt_prefix
338
+ caret_styles = rails_production? ? %i[red bold] : %i[cyan bold]
339
+ IRB.conf[:PROMPT] ||= {}
340
+ IRB.conf[:PROMPT][:PRETTY_IRB] = {
341
+ PROMPT_I: "#{prefix} #{style_text('❯ ', *caret_styles)}",
342
+ PROMPT_N: "#{prefix} #{style_text('│ ', :light_black)}",
343
+ PROMPT_S: "#{prefix} #{style_text('┆ ', :light_black)}",
344
+ PROMPT_C: "#{prefix} #{style_text('· ', :light_black)}",
345
+ RETURN: "=> %s\n"
346
+ }
347
+ IRB.conf[:PROMPT_MODE] = :PRETTY_IRB
348
+ IRB.conf[:SAVE_HISTORY] ||= 1000
349
+ end
350
+
351
+ def build_prompt_prefix
352
+ base = style_text("[%03n]", :light_black)
353
+ badge = rails_env_badge
354
+ badge ? "#{base} #{badge}" : base
355
+ end
356
+
357
+ def rails_production?
358
+ defined?(Rails) && Rails.respond_to?(:env) && Rails.env.to_s == "production"
359
+ end
360
+
361
+ def rails_env_badge
362
+ return nil unless PrettyIRB.configuration.rails_env_prompt
363
+ return nil unless defined?(Rails) && Rails.respond_to?(:env) && Rails.env
364
+
365
+ env = Rails.env.to_s
366
+ return nil if env.empty?
367
+
368
+ styled_env = case env
369
+ when "development" then style_text("dev", :cyan)
370
+ when "test" then style_text("test", :light_blue)
371
+ when "production" then style_text("PROD", :red, :bold)
372
+ else style_text(env, :yellow)
373
+ end
374
+
375
+ if rails_sandbox?
376
+ "(#{styled_env}#{style_text(':', :light_black)}#{style_text('sandbox', :yellow)})"
377
+ else
378
+ "(#{styled_env})"
379
+ end
380
+ end
381
+
382
+ def install_irb_rc_hook
383
+ prev_rc = IRB.conf[:IRB_RC]
384
+ IRB.conf[:IRB_RC] = proc do |ctx|
385
+ prev_rc.call(ctx) if prev_rc.respond_to?(:call)
386
+ ctx.prompt_mode = :PRETTY_IRB
387
+ ctx.auto_indent_mode = true
388
+ ctx.workspace.main.extend(PrettyIRB::IRBCommands)
389
+ if !@using_rails_irb_console && defined?(Rails::ConsoleMethods)
390
+ ctx.workspace.main.extend(Rails::ConsoleMethods)
391
+ end
392
+ IRB.conf[:MAIN_CONTEXT] ||= ctx
393
+ end
394
+ end
395
+
396
+ def style_text(text, *styles)
397
+ PrettyIRB::UI.paint(text, *styles)
398
+ end
399
+
400
+ # ── Hooks ─────────────────────────────────────────────────────────────────
401
+
402
+ def install_output_hook
403
+ return if IRB::Irb.ancestors.include?(OutputHook)
404
+
405
+ IRB::Irb.prepend(OutputHook)
406
+ rescue => e
407
+ warn style_text(" pretty_irb: output hook skipped (#{e.message})", :light_black)
408
+ end
409
+
410
+ def install_eval_hook
411
+ return if IRB::Context.ancestors.include?(EvalHook)
412
+
413
+ IRB::Context.prepend(EvalHook)
414
+ rescue => e
415
+ warn style_text(" pretty_irb: eval hook skipped (#{e.message})", :light_black)
416
+ end
417
+
418
+ def install_exception_hook
419
+ return unless IRB::Irb.instance_methods.include?(:handle_exception)
420
+ return if IRB::Irb.ancestors.include?(ExceptionHook)
421
+
422
+ IRB::Irb.prepend(ExceptionHook)
423
+ rescue => e
424
+ warn style_text(" pretty_irb: exception hook skipped (#{e.message})", :light_black)
425
+ end
426
+
427
+ def install_rails_irb_hook
428
+ return if IRB::Irb.ancestors.include?(RailsIRBInitHook)
429
+
430
+ IRB::Irb.prepend(RailsIRBInitHook)
431
+ rescue => e
432
+ warn style_text(" pretty_irb: rails init hook skipped (#{e.message})", :light_black)
433
+ end
434
+ end
435
+ end