pretty_irb 0.1.2 → 0.1.3

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: de7427c34f3650136e317c86f5b32d53a24c4a883c4f3c70dc07684653cab46b
4
+ data.tar.gz: 258568ee7bc6cc9d42c20d56f7ad80ca16f0a7c63b3b303f0823f405ac67cfaa
5
5
  SHA512:
6
- metadata.gz: 2d45064a505563e76cc975821c9caf2745885306a50e450e6dafebce7c9ec73270a37201151436db83caf1fe67502bcb8763e216910828b48fd2da6a9095e965
7
- data.tar.gz: 64623e8b5a19298cdb41808c38be3b9f241bd9133b5b0b0354851c7f100a98e5db56ddca2e501ff7ef61f59e637e5f270a3d7045cbe7b95da2bc4eba18fe50a3
6
+ metadata.gz: 3cc973dea626a8ee900e7fbbf26b62d4a941a00fb0f95b137bad0d388c0989e6cfec92d33a35d707f37ddfaac64c24dbe2b5ab8b31ab2b5c48d6c14324368a2e
7
+ data.tar.gz: 02f2fcd034a86279c301c20497d0fdba10672e528a3df3026c1b6ac173beb13199a0332341ef67f141c92c034bfc23ba5622376f9db61ba8fd2ccca9bd38ac94
@@ -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
@@ -1,66 +1,89 @@
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
+ class << self
7
+ # "=> value" — identical to how IRB shows results
8
+ def format_result(output)
9
+ "=> ".white + format_output(output)
10
+ rescue
11
+ "=> #{output}"
12
+ end
13
+
14
+ def format_output(value)
15
+ 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
27
+ end
28
+ rescue
29
+ value.to_s
30
+ end
31
+
32
+ # Rouge Monokai syntax highlight for Ruby code (used in history / snippets)
33
+ def highlight_code(code)
34
+ formatter = Rouge::Formatters::Terminal256.new(Rouge::Themes::Monokai.new)
35
+ lexer = Rouge::Lexers::Ruby.new
36
+ formatter.format(lexer.lex(code))
37
+ rescue
38
+ code
39
+ end
40
+
41
+ # "ErrorType: message" — red type, dim message
42
+ def format_error(type, message = nil)
43
+ if message
44
+ type.red.bold + ": ".red + message.light_red
45
+ else
46
+ type.red.bold
47
+ end
48
+ rescue
49
+ "#{type}: #{message}"
50
+ end
51
+
52
+ def format_success(message)
53
+ message.green
54
+ rescue
55
+ message
56
+ end
57
+
58
+ def format_warning(message)
59
+ message.yellow
60
+ rescue
61
+ message
62
+ end
63
+
64
+ private
65
+
66
+ def format_array(arr)
67
+ return "[]".white if arr.empty?
68
+
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
72
+ rescue
73
+ arr.inspect
74
+ end
75
+
76
+ def format_hash(hash)
77
+ return "{}".white if hash.empty?
78
+
79
+ pairs = hash.first(30).map do |k, v|
80
+ format_output(k) + " => ".white + format_output(v)
81
+ end
82
+ pairs << "… (#{hash.length} total)".light_black if hash.length > 30
83
+ "{".white + pairs.join(", ".white) + "}".white
84
+ rescue
85
+ hash.inspect
86
+ end
87
+ end
88
+ end
89
+ end
@@ -50,8 +50,15 @@ module PrettyIRB
50
50
 
51
51
  # Export history to file
52
52
  def export(filename)
53
- File.write(filename, format_for_export)
54
- "✓ History exported to #{filename}"
53
+ 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")
56
+ return "❌ Refusing to write to system path: #{path}"
57
+ end
58
+ File.write(path, format_for_export)
59
+ "✓ History exported to #{path}"
60
+ rescue => e
61
+ "❌ Export failed: #{e.message}"
55
62
  end
56
63
 
57
64
  private
@@ -0,0 +1,139 @@
1
+ module PrettyIRB
2
+ # Methods mixed into IRB's main object at startup.
3
+ # Names intentionally avoid IRB's own built-in commands
4
+ # (history, hist, help, ls, exit, quit, …) because IRB intercepts
5
+ # those at the statement level before method dispatch.
6
+ module IRBCommands
7
+
8
+ # ── commands ──────────────────────────────────────────────────────────
9
+ # Show all pretty_irb extras (use IRB's own `help` for core IRB commands)
10
+
11
+ def commands
12
+ section = ->(t) { puts "\n #{t}".cyan }
13
+ row = ->(n, d) { puts " #{"%-40s" % n}#{d.light_black}" }
14
+
15
+ puts "\n pretty_irb v#{PrettyIRB::VERSION}".cyan + \
16
+ " · type help for core IRB commands".light_black
17
+ puts " " + ("─" * 60).light_black
18
+
19
+ section.("Snippets")
20
+ row.("snippet", "list all saved snippets")
21
+ row.("snippet(:save, 'n', 'code')", "save a snippet")
22
+ row.("snippet(:load, 'name')", "load and run snippet")
23
+ row.("snippet(:show, 'name')", "show snippet code")
24
+ row.("snippet(:delete, 'name')", "delete snippet")
25
+ row.("snippet(:search, 'keyword')", "search snippets")
26
+
27
+ section.("Variables")
28
+ row.("vars", "list all local variables")
29
+ row.("vars('name')", "inspect one variable")
30
+ row.("vars(type: 'String')", "filter by type")
31
+ row.("vars(search: 'kw')", "search by name")
32
+ row.("vars(memory: true)", "estimate memory usage")
33
+
34
+ section.("Benchmark")
35
+ row.("bench('code')", "time code (1 000 iterations)")
36
+ row.("bench('a', vs: 'b')", "compare two snippets")
37
+ row.("bench('code', memory: true)", "GC / object allocation profile")
38
+ row.("bench('code', iterations: 5000)", "custom iteration count")
39
+
40
+ section.("Cheatsheet")
41
+ row.("cheat", "general Ruby reference")
42
+ row.("cheat('array')", "topic: array hash string enumerable file regex date")
43
+
44
+ section.("Docs")
45
+ row.("explain('method')", "explain a Ruby method")
46
+ row.("example('topic')", "code examples for a topic")
47
+ row.("practices('topic')", "best practices for a topic")
48
+ row.("ref('keyword')", "quick reference lookup")
49
+
50
+ puts "\n tip: IRB built-ins — ".light_black +
51
+ "history ls show_source show_doc help exit".white
52
+ puts
53
+ nil
54
+ end
55
+
56
+ # ── snippets ──────────────────────────────────────────────────────────
57
+
58
+ def snippet(action = :list, name = nil, code = nil, description: "")
59
+ mgr = PrettyIRB::Shell.snippet_manager
60
+ case action.to_sym
61
+ when :list
62
+ puts mgr.list
63
+ when :save
64
+ puts mgr.save(name.to_s, code.to_s, description)
65
+ when :load
66
+ src = mgr.load(name.to_s)
67
+ if src.is_a?(String) && !src.start_with?("❌")
68
+ puts " loaded: #{name}".cyan
69
+ puts PrettyIRB::Formatter.highlight_code(src)
70
+ eval(src, IRB.CurrentContext.workspace.binding)
71
+ else
72
+ puts src
73
+ end
74
+ when :show
75
+ puts PrettyIRB::Formatter.highlight_code(mgr.show(name.to_s))
76
+ when :delete
77
+ puts mgr.delete(name.to_s)
78
+ when :search
79
+ puts mgr.search(name.to_s)
80
+ else
81
+ puts " actions: :list :save :load :show :delete :search".yellow
82
+ end
83
+ nil
84
+ end
85
+
86
+ # ── variables ─────────────────────────────────────────────────────────
87
+
88
+ def vars(name = nil, type: nil, search: nil, memory: false)
89
+ inspector = PrettyIRB::VariableInspector.new(IRB.CurrentContext.workspace.binding)
90
+ if memory then puts inspector.memory_usage
91
+ elsif type then puts inspector.by_type(type.to_s)
92
+ elsif search then puts inspector.search(search.to_s)
93
+ elsif name then puts inspector.inspect_var(name.to_s)
94
+ else puts inspector.list_variables
95
+ end
96
+ nil
97
+ end
98
+
99
+ # ── benchmark ─────────────────────────────────────────────────────────
100
+
101
+ def bench(code, vs: nil, memory: false, iterations: 1000)
102
+ b = IRB.CurrentContext.workspace.binding
103
+ if memory then puts PrettyIRB::Benchmarker.profile_memory(code.to_s, iterations, b)
104
+ elsif vs then puts PrettyIRB::Benchmarker.compare(code.to_s, vs.to_s, iterations, b)
105
+ else puts PrettyIRB::Benchmarker.benchmark(code.to_s, iterations, b)
106
+ end
107
+ nil
108
+ end
109
+
110
+ # ── cheatsheet ────────────────────────────────────────────────────────
111
+
112
+ def cheat(topic = nil)
113
+ puts PrettyIRB::CheatSheet.show(topic)
114
+ nil
115
+ end
116
+
117
+ # ── docs ──────────────────────────────────────────────────────────────
118
+
119
+ def explain(method_name)
120
+ puts PrettyIRB::AIHelper.explain_method(method_name.to_s)
121
+ nil
122
+ end
123
+
124
+ def example(topic)
125
+ puts PrettyIRB::AIHelper.get_example(topic.to_s)
126
+ nil
127
+ end
128
+
129
+ def practices(topic)
130
+ puts PrettyIRB::AIHelper.best_practices(topic.to_s)
131
+ nil
132
+ end
133
+
134
+ def ref(keyword)
135
+ puts PrettyIRB::AIHelper.quick_reference(keyword.to_s)
136
+ nil
137
+ end
138
+ end
139
+ end
@@ -1,387 +1,154 @@
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
+ class << self
13
+ attr_accessor :history_manager, :snippet_manager
14
+ end
15
+
16
+ def initialize(config = {})
17
+ @config = config
18
+ end
19
+
20
+ def run
21
+ STDOUT.sync = true
22
+ puts BANNER.cyan
23
+
24
+ Shell.history_manager = HistoryManager.new
25
+ Shell.snippet_manager = SnippetManager.new
26
+
27
+ # Step 1: patch IRB classes before anything else
28
+ install_output_hook
29
+ install_eval_hook
30
+
31
+ # Step 2: IRB.setup populates IRB.conf (and resets IRB_RC to nil).
32
+ # Must happen before we configure anything.
33
+ IRB.setup(nil)
34
+
35
+ # Step 3: detect the terminal environment and tune input mode.
36
+ # Must happen AFTER setup (so USE_MULTILINE can be overridden)
37
+ # but BEFORE IRB::Irb.new reads those values.
38
+ tune_input_mode
39
+
40
+ # Step 4: prompt — IRB.conf[:PROMPT] now exists.
41
+ configure_prompt
42
+
43
+ # Step 5: IRB_RC — fires inside irb.run, after IRB::Irb.new but before
44
+ # the first prompt. Safe place for ctx-level settings.
45
+ prev_rc = IRB.conf[:IRB_RC]
46
+ IRB.conf[:IRB_RC] = proc do |ctx|
47
+ prev_rc.call(ctx) if prev_rc.respond_to?(:call)
48
+ ctx.prompt_mode = :PRETTY_IRB
49
+ ctx.auto_indent_mode = true
50
+ ctx.workspace.main.extend(PrettyIRB::IRBCommands)
51
+ end
52
+
53
+ # Step 6: start the REPL directly to avoid a second IRB.setup call.
54
+ irb = IRB::Irb.new
55
+ IRB.conf[:MAIN_CONTEXT] = irb.context
56
+ irb.run(IRB.conf)
57
+ end
58
+
59
+ private
60
+
61
+ # ── Terminal / input detection ────────────────────────────────────────────
62
+ # On Windows, Reline uses a Win32 API console handler. In "legacy
63
+ # console" mode (old conhost.exe — PowerShell or cmd.exe without
64
+ # Windows Terminal) Alt+key sequences are read in a way that blocks the
65
+ # input loop, freezing the entire REPL.
66
+ #
67
+ # We detect this and switch to single-line (non-Reline) input, which
68
+ # avoids the freeze while keeping colour, our custom prompt, and all
69
+ # pretty_irb commands. Full features (auto-indent, │ continuation, tab
70
+ # completion) work when the user opens the same shell inside Windows
71
+ # Terminal.
72
+
73
+ def tune_input_mode
74
+ return unless Gem.win_platform?
75
+
76
+ legacy = begin
77
+ Reline::IOGate.instance_variable_get(:@legacy_console)
78
+ rescue
79
+ false
80
+ end
81
+
82
+ if legacy
83
+ IRB.conf[:USE_MULTILINE] = false # avoid Reline Windows hang
84
+ IRB.conf[:USE_AUTOCOMPLETE] = false
85
+ puts (
86
+ " legacy console detected — Alt+key sequences freeze Reline.\n" \
87
+ " Running in single-line mode. Open Windows Terminal for\n" \
88
+ " full auto-indent and tab-complete support."
89
+ ).light_black
90
+ end
91
+ rescue
92
+ nil
93
+ end
94
+
95
+ # ── Prompt ────────────────────────────────────────────────────────────────
96
+
97
+ def configure_prompt
98
+ IRB.conf[:PROMPT][:PRETTY_IRB] = {
99
+ PROMPT_I: "[%03n]".light_black + " ".cyan,
100
+ PROMPT_N: "[%03n]".light_black + "│ ".light_cyan,
101
+ PROMPT_S: "[%03n]".light_black + "│ ".light_cyan,
102
+ PROMPT_C: "[%03n]".light_black + "│ ".light_cyan,
103
+ RETURN: "=> %s\n"
104
+ }
105
+ IRB.conf[:PROMPT_MODE] = :PRETTY_IRB
106
+ IRB.conf[:SAVE_HISTORY] = 1000
107
+ end
108
+
109
+ # ── Output hook ───────────────────────────────────────────────────────────
110
+
111
+ def install_output_hook
112
+ IRB::Irb.prepend(Module.new do
113
+ def output_value(omit = false)
114
+ puts PrettyIRB::Formatter.format_result(@context.last_value)
115
+ end
116
+ end)
117
+ rescue => e
118
+ warn " pretty_irb: output hook skipped (#{e.message})".light_black
119
+ end
120
+
121
+ # ── Eval hook ─────────────────────────────────────────────────────────────
122
+ # IRB >= 1.4 passes IRB::Statement objects, not plain Strings.
123
+ # Extract the raw code string for our features; always forward the
124
+ # original object unchanged so IRB's own command dispatch still works.
125
+
126
+ def install_eval_hook
127
+ IRB::Context.prepend(Module.new do
128
+ def evaluate(code, line_no, *args, **kwargs, &block)
129
+ raw = code.is_a?(String) ? code
130
+ : code.respond_to?(:code) ? code.code.to_s
131
+ : nil
132
+
133
+ if raw
134
+ corrected = PrettyIRB::AutoCorrector.auto_correct_code(raw)
135
+ if corrected.strip != raw.strip
136
+ warn " ↻ auto-corrected: #{corrected.strip}".yellow
137
+ end
138
+ end
139
+
140
+ result = begin
141
+ super(code, line_no, *args, **kwargs, &block)
142
+ rescue ArgumentError
143
+ super(code, line_no)
144
+ end
145
+
146
+ PrettyIRB::Shell.history_manager&.add(raw.strip) if raw && !raw.strip.empty?
147
+ result
148
+ end
149
+ end)
150
+ rescue => e
151
+ warn " pretty_irb: eval hook skipped (#{e.message})".light_black
152
+ end
153
+ end
154
+ end
@@ -11,6 +11,7 @@ module PrettyIRB
11
11
 
12
12
  # Save a code snippet
13
13
  def save(name, code, description = "")
14
+ return "❌ Invalid snippet name. Use only letters, numbers, underscores, and hyphens." unless valid_name?(name)
14
15
  filename = snippet_path(name)
15
16
 
16
17
  snippet = {
@@ -27,6 +28,7 @@ module PrettyIRB
27
28
 
28
29
  # Load a snippet
29
30
  def load(name)
31
+ return "❌ Invalid snippet name." unless valid_name?(name)
30
32
  filename = snippet_path(name)
31
33
  return "❌ Snippet '#{name}' not found" unless File.exist?(filename)
32
34
 
@@ -73,6 +75,7 @@ module PrettyIRB
73
75
 
74
76
  # Delete a snippet
75
77
  def delete(name)
78
+ return "❌ Invalid snippet name." unless valid_name?(name)
76
79
  filename = snippet_path(name)
77
80
  return "❌ Snippet '#{name}' not found" unless File.exist?(filename)
78
81
 
@@ -82,6 +85,7 @@ module PrettyIRB
82
85
 
83
86
  # Show snippet details
84
87
  def show(name)
88
+ return "❌ Invalid snippet name." unless valid_name?(name)
85
89
  filename = snippet_path(name)
86
90
  return "❌ Snippet '#{name}' not found" unless File.exist?(filename)
87
91
 
@@ -100,6 +104,10 @@ module PrettyIRB
100
104
  FileUtils.mkdir_p(SNIPPET_DIR) unless Dir.exist?(SNIPPET_DIR)
101
105
  end
102
106
 
107
+ def valid_name?(name)
108
+ name.to_s.match?(/\A[a-zA-Z0-9_-]+\z/)
109
+ end
110
+
103
111
  def snippet_path(name)
104
112
  File.join(SNIPPET_DIR, "#{name}.json")
105
113
  end
@@ -1,3 +1,3 @@
1
1
  module PrettyIRB
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.3"
3
3
  end
data/lib/pretty_irb.rb CHANGED
@@ -13,6 +13,7 @@ require_relative "pretty_irb/snippet_manager"
13
13
  require_relative "pretty_irb/variable_inspector"
14
14
  require_relative "pretty_irb/benchmarker"
15
15
  require_relative "pretty_irb/cheat_sheet"
16
+ require_relative "pretty_irb/irb_commands"
16
17
  require_relative "pretty_irb/shell"
17
18
 
18
19
  module PrettyIRB
data/pretty_irb.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = "pretty_irb"
3
- spec.version = "0.1.2"
3
+ spec.version = "0.1.3"
4
4
  spec.authors = ["Jayesh"]
5
5
  spec.email = ["jayesh@example.com"]
6
6
  spec.summary = "A pretty IRB replacement with auto-correct and beautiful fonts"
@@ -37,6 +37,7 @@ Gem::Specification.new do |spec|
37
37
  # Allow any version >= 1.5 but < 3.0 to avoid conflicts with older constraints.
38
38
  spec.add_dependency "did_you_mean", ">= 1.5", "< 3.0" # Auto-correct suggestions
39
39
  spec.add_dependency "reline", "~> 0.3" # Enhanced readline
40
+ spec.add_dependency "benchmark" # Performance measurement
40
41
 
41
42
  # Development dependencies
42
43
  spec.add_development_dependency "bundler", "~> 2.0"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pretty_irb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jayesh
@@ -85,6 +85,20 @@ dependencies:
85
85
  - - "~>"
86
86
  - !ruby/object:Gem::Version
87
87
  version: '0.3'
88
+ - !ruby/object:Gem::Dependency
89
+ name: benchmark
90
+ requirement: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ type: :runtime
96
+ prerelease: false
97
+ version_requirements: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
88
102
  - !ruby/object:Gem::Dependency
89
103
  name: bundler
90
104
  requirement: !ruby/object:Gem::Requirement
@@ -173,6 +187,7 @@ files:
173
187
  - lib/pretty_irb/cheat_sheet.rb
174
188
  - lib/pretty_irb/formatter.rb
175
189
  - lib/pretty_irb/history_manager.rb
190
+ - lib/pretty_irb/irb_commands.rb
176
191
  - lib/pretty_irb/shell.rb
177
192
  - lib/pretty_irb/snippet_manager.rb
178
193
  - lib/pretty_irb/variable_inspector.rb