emonti-wxirb 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. data/README.rdoc +173 -0
  2. data/bin/wxirb +14 -0
  3. data/lib/wxirb.rb +393 -0
  4. metadata +64 -0
data/README.rdoc ADDED
@@ -0,0 +1,173 @@
1
+
2
+ = WxIRB
3
+
4
+ This is a GUI "irb-alike" console based on WxRuby. I wrote this because I
5
+ needed a better way to prototype and debug my wxruby applications as I was
6
+ developing them. WxIRB puts you "inside" a Wx::App.run event loop, which
7
+ lets you easily play with window objects during run-time.
8
+
9
+ This is mostly just a port of why_the_lucky_stiff's Shoes GUI irb example to
10
+ wxruby with the addition of a command history and a few other convenience
11
+ methods added in the window classes.
12
+
13
+ Credit to Why.
14
+ See: http://github.com/why/shoes/blob/master/samples/expert-irb.rb
15
+
16
+ == Installation
17
+
18
+ WxIRB is available as a gem from github.
19
+
20
+ gem sources -a http://gems.github.com #(you only have to do this once)
21
+ sudo gem install wxirb
22
+
23
+ Or you can install it manually:
24
+
25
+ git clone git://github.com/emonti/wxirb.git
26
+ sudo cp wxirb/lib/wxirb.rb /usr/lib/ruby/1.8/site_ruby/1.8 # or wherever
27
+ sudo cp wxirb/bin/wxirb /usr/local/bin # or wherever
28
+
29
+ == Keyboard Interaction
30
+
31
+ WxIRB is designed to allow you to edit multi-line ruby statements in the
32
+ input window. The keyboard commands in the input text area are what you'
33
+ d expect in a multi-line text-box with some additional special keyboard
34
+ modifiers:
35
+
36
+ * ENTER : runs a statement through the mock IRB object. (what you'd expect)
37
+ * META+ENTER : sends a newline inside the window instead of running a command
38
+ * META+UP-ARROW : scroll up in history
39
+ * META+DOWN-ARROW : scroll down in history
40
+
41
+ WxIRB does not have a "run" button. Thank goodness!
42
+
43
+ The output text area is read-only from the UI. Tabbing focus from the output
44
+ text area should land you back in the input text area.
45
+
46
+
47
+ == Interacting with the WxIRB Window Objects
48
+
49
+ When running wx_irb.rb directly a global variable named '$wxirb' is created
50
+ for you which holds a reference to the WxIRB::BaseFrame window you are using.
51
+
52
+ This is done so you can easily access the UI frame object and children from
53
+ within the actual interface. A few convenience methods and accessors are
54
+ exposed this way:
55
+
56
+ * '$wxirb.clear' lets you clear the output window
57
+
58
+ * '$wxirb.history' returns the command history object (basically an array)
59
+
60
+ * '$wxirb.histdump(s=0, e=-1)' prints the history to the output text area.
61
+ to the output text area. Takes optional start and indexes for viewing just
62
+ a slice of the history array.
63
+
64
+ * '$wxirb.set_binding(bind)' sets the object binding to 'bind' which is used
65
+ when running eval() on user input. See also, 'Object Binding'.
66
+
67
+
68
+ == WxIRB CommandHistory
69
+
70
+ WxIRB maintains a persistent history log. The WxIRB history uses a separate
71
+ file from IRB which is defined by WxIRB::CommandHistory::HISTFILE. It is
72
+ actually just a YAMLized array.
73
+
74
+ By default, this is $HOME/.wxirb_history (no way of 'convenient' way of
75
+ configuring this right now)
76
+
77
+ History is implemented in the WxIRB::CommandHistory class. This is basically
78
+ an array with a few convenience methods and accessors:
79
+
80
+ * 'hpos' is an accessor to the history array position.
81
+
82
+ * 'prev' moves hpos back one and returns the history value at that position
83
+
84
+ * 'next' does the same thing, but forward
85
+
86
+ * '<<' is an override for the Array superclass that just updates the history
87
+ position variable.
88
+
89
+ * 'save!' saves current history to the persistent history file
90
+
91
+ * 'save' saves to the persistent history file only if the history has changed.
92
+ (this is used for an evt_idle event handler by the input text window)
93
+
94
+ * 'clear' empties the history array and persistent history file
95
+
96
+
97
+ == Output to WxIRB
98
+
99
+ The WxIRB::BaseFrame object also has an 'output' accessor which returns a
100
+ reference to the OutputTextCtrl text window half of the display. This object
101
+ has a few methods to make it usable (in duck-typing cases) as an IO object.
102
+ Note: This doesn't actually inherit from or implement all of IO class, however.
103
+
104
+ * '<<' outputs to the output window
105
+
106
+ * 'puts' prints directly to the output text area like 'IO.puts' and returns
107
+ nil.
108
+
109
+ * 'print' prints directly to the output text area like 'IO.print'
110
+ and returns nil.
111
+
112
+ * 'write' prints directly to the output text area like 'IO.write' and returns
113
+ the number of bytes written.
114
+
115
+ * Note: write, print, and puts all use brown text to differentiate from other
116
+ output.
117
+
118
+ * 'close', 'closed?' and 'flush' are also all defined but just emulate an
119
+ IO object with their return values. Other than that, they do nothing.
120
+
121
+
122
+ == Object Binding
123
+
124
+ Originally, WxIRB just ran 'eval' using the TOPLEVEL_BINDING (aka main)
125
+ This is how you usually run IRB. However, it may be desirable to instantiate
126
+ a wxirb console bound to a different namespace for debugging specific objects
127
+ and whatnot. So a few ways to manage bindings were added.
128
+
129
+ * WxIRB::BaseFrame.new now accepts an optional binding parameter. For example:
130
+
131
+ >> WxIRB::BaseFrame.new(nil, :binding => TOPLEVEL_BINDING)
132
+
133
+ By default, new WxIRB::BaseFrame objects will bind to themselves. Meaning,
134
+ commands will run in the scope of the WxIRB::BaseFrame scope itself.
135
+ TOPLEVEL_BINDING makes the scope 'main'.
136
+
137
+ When running 'wxirb.rb' directly, you will start with TOPLEVEL_BINDING, but
138
+ you can use the 'set_binding' method of $wxirb to change the binding on the
139
+ fly.
140
+
141
+ * As mentioned above, WxIRB::BaseFrame also has an instance method called
142
+ set_binding. This method lets you change WxIRB's binding on the fly.
143
+ Here's a short example from inside WxIRB running 'wxirb.rb' directly:
144
+
145
+ >> self
146
+ => main
147
+ >> $wxirb
148
+ => #<WxIRB::BaseFrame:0x51e8c0>
149
+ >> $wxirb.set_binding $wxirb.instance_eval { binding }
150
+ => #<Binding:0x17235fe8>
151
+ >> self
152
+ => #<WxIRB::BaseFrame:0x51e8c0>
153
+ >> @output
154
+ => #<WxIRB::InputTextCtrl:0x51d5b0>
155
+ >> @output.puts "hello self"
156
+ hello self
157
+ => nil
158
+ >> set_binding TOPLEVEL_BINDING
159
+ => #<Binding:0x296d0>
160
+ >> self
161
+ => main
162
+
163
+
164
+ == BUGS
165
+
166
+ * Running statements gets slow when the Output window gets very full. Not
167
+ really sure why this is, but running 'wxirb.clear' periodically helps.
168
+
169
+ * An effort is made to rescue most exceptions, but sometimes wxirb will
170
+ close due to an un-handled exception. Regular 'irb' does this too
171
+ sometimes... so I don't feel too bad.
172
+
173
+
data/bin/wxirb ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ begin
3
+ require 'rubygems'
4
+ rescue LoadError
5
+ end
6
+
7
+ require 'wx'
8
+ require 'wxirb'
9
+
10
+ Wx::App.run do
11
+ $wxirb = WxIRB::BaseFrame.new(nil, :binding => TOPLEVEL_BINDING)
12
+ $wxirb.show
13
+ end
14
+
data/lib/wxirb.rb ADDED
@@ -0,0 +1,393 @@
1
+ #!/usr/bin/env ruby
2
+ begin
3
+ require 'rubygems'
4
+ rescue LoadError
5
+ end
6
+ require 'wx'
7
+ require 'yaml'
8
+ require 'irb/ruby-lex'
9
+ require 'stringio'
10
+
11
+ # This class is stolen almost verbatim from why_the_lucky_stiff's Shoes
12
+ # example. (http://github.com/why/shoes/blob/master/samples/expert-irb.rb)
13
+ # He gets all the credit.
14
+ class MimickIRB < RubyLex
15
+ attr_accessor :started
16
+
17
+ class Continue < StandardError; end
18
+ class Empty < StandardError; end
19
+
20
+ def initialize(bind=TOPLEVEL_BINDING)
21
+ set_binding(bind)
22
+ super()
23
+ set_input(StringIO.new)
24
+ end
25
+
26
+ def set_binding(bind)
27
+ if bind.is_a? Binding
28
+ @bind = bind
29
+ else
30
+ raise "Invalid binding #{bind.inspect}"
31
+ end
32
+ end
33
+
34
+ def run(str)
35
+ obj = nil
36
+ @io << str
37
+ @io.rewind
38
+ unless l = lex
39
+ raise Empty if @line == ''
40
+ else
41
+ case l.strip
42
+ when "reset"
43
+ @line = ""
44
+ else
45
+ @line << l << "\n"
46
+ if @ltype or @continue or @indent > 0
47
+ raise Continue
48
+ end
49
+ end
50
+ end
51
+ unless @line.empty?
52
+ obj = eval @line, @bind, "(irb)", @line_no
53
+ end
54
+ @line_no += @line.scan(/\n/).length
55
+ @line = ''
56
+ @exp_line_no = @line_no
57
+
58
+ @indent = 0
59
+ @indent_stack = []
60
+
61
+ $stdout.rewind
62
+ output = $stdout.read
63
+ $stdout.truncate(0)
64
+ $stdout.rewind
65
+ [output, obj]
66
+ rescue Object => e
67
+ case e when Empty, Continue
68
+ else @line = ""
69
+ end
70
+ raise e
71
+ ensure
72
+ set_input(StringIO.new)
73
+ end
74
+ end
75
+
76
+
77
+ # Docs say there's a KeyEvent.cmd_down which is platform independent.
78
+ # But they LIE so we create our own...
79
+ class Wx::KeyEvent
80
+ case Wx::PLATFORM
81
+ when "WXMAC"
82
+ def inputmod_down; meta_down; end
83
+ when "WXMSW"
84
+ def inputmod_down; alt_down; end
85
+ end
86
+ end
87
+
88
+
89
+ module WxIRB
90
+ # WxIRB maintains a persistent history log. The WxIRB history uses a separate
91
+ # file from IRB which is defined by WxIRB::CommandHistory::HISTFILE. It is
92
+ # actually just a YAMLized array.
93
+ #
94
+ # By default, the file is $HOME/.wxirb_history (no 'convenient' way of
95
+ # configuring this right now aside from changing the constant)
96
+ #
97
+ # History is implemented in the WxIRB::CommandHistory class. This is
98
+ # basically just an Array with a few convenience methods and accessors.
99
+ class CmdHistory < Array
100
+ # an accessor to the history array position.
101
+ attr_accessor :hpos
102
+
103
+ # location of the persistent history file
104
+ HISTFILE = "#{ENV["HOME"]}/.wxirb_history"
105
+
106
+ # Initializes a CommandHistory object and loads persistent history
107
+ # from the file specified in HISTFILE
108
+ def initialize(*opts)
109
+ super(*opts)
110
+ @hpos = nil
111
+ begin
112
+ self.replace YAML.load_file(HISTFILE)
113
+ @hpos = self.size-1
114
+ rescue
115
+ STDERR.puts "Note - error restoring history: #{$!.inspect}"
116
+ end
117
+ end
118
+
119
+ # moves hpos back one and returns the history value at that position
120
+ def prev
121
+ return nil if self.empty?
122
+ val = self[@hpos].to_s
123
+ @hpos -= 1 unless @hpos == 0
124
+ return val
125
+ end
126
+
127
+ # moves hpos forward one and returns the history value at that position
128
+ def next
129
+ if not self.empty? and @hpos != self.size-1
130
+ @hpos += 1
131
+ self[@hpos].to_s
132
+ end
133
+ end
134
+
135
+ # An override for the Array superclass that just updates the history
136
+ # position variable. This also ensures that history elements are appended
137
+ # as strings.
138
+ def << (val)
139
+ @hpos = self.size
140
+ @changed=true
141
+ super(val.to_s)
142
+ end
143
+
144
+ # empties the history array and persistent history file
145
+ def clear
146
+ self.replace([])
147
+ self.save!
148
+ end
149
+
150
+ # saves current history to the persistent history file
151
+ def save!
152
+ ret=nil
153
+ begin
154
+ ret=File.open(HISTFILE, "w") {|f| f.write(self.to_yaml) }
155
+ rescue Errno::ENOENT
156
+ STDERR.puts "Error: couldn't save history - #{$!}"
157
+ end
158
+ @changed=nil
159
+ ret
160
+ end
161
+
162
+ # saves to the persistent history file only if the history has changed.
163
+ # (this is used for an evt_idle event handler by the input text window)
164
+ def save(force=false)
165
+ return nil unless @changed or force
166
+ self.save!
167
+ end
168
+ end
169
+
170
+
171
+ # Keyboard commands in the input text area are what you'd expect in a
172
+ # multi-line textbox with some additional special keyboard modifiers.
173
+ # See InputTextCtrl#on_char
174
+ class InputTextCtrl < Wx::TextCtrl
175
+ attr_accessor :history
176
+ include Wx
177
+ STYLE = TE_PROCESS_TAB|TE_PROCESS_ENTER|WANTS_CHARS|TE_MULTILINE
178
+
179
+ def initialize(parent, output, mirb)
180
+ super(parent, :style => STYLE)
181
+ @history = CmdHistory.new
182
+ @output = output
183
+
184
+ @stdout_save = $stdout
185
+ $stdout = StringIO.new
186
+ @mirb = mirb
187
+ evt_idle :on_idle
188
+ evt_char :on_char
189
+
190
+ @font = Wx::Font.new(10, Wx::MODERN, Wx::NORMAL, Wx::NORMAL)
191
+ set_default_style Wx::TextAttr.new(Wx::BLACK, Wx::WHITE, @font)
192
+
193
+ paint do |dc|
194
+ b_height = dc.get_text_extent("@", @font)[1] * 4
195
+ cache_best_size Wx::Size.new(self.size.width, b_height)
196
+ end
197
+ end
198
+
199
+ # Fires on Wx::IdleEvent - saves persistent history if history has changed
200
+ def on_idle(evt)
201
+ history.save
202
+ end
203
+
204
+ # Fires on text input events.
205
+ # Implements a few special keyboard handlers
206
+ # * META+ENTER : sends a newline inside the input window instead of
207
+ # actually running a command
208
+ # * META+UP-ARROW : scroll up in history
209
+ # * META+DOWN-ARROW : scroll down in history
210
+ def on_char(evt)
211
+ k = evt.key_code
212
+ mflag = evt.modifiers
213
+
214
+ if [MOD_NONE, MOD_SHIFT].include?(mflag) and (0x20..0x7f).include?(k)
215
+ evt.skip()
216
+ return
217
+ end
218
+
219
+ case k
220
+ when K_RETURN
221
+ if evt.inputmod_down
222
+ # multi-line command uses meta-down-arrow for newline
223
+ self.write_text("\n")
224
+ else
225
+ @history << self.value
226
+ run self.value
227
+ self.clear
228
+ end
229
+ return
230
+ when (evt.inputmod_down and K_UP)
231
+ if hist=history.prev
232
+ self.value = hist
233
+ self.set_insertion_point_end
234
+ return
235
+ end
236
+ when (evt.inputmod_down and K_DOWN)
237
+ if hist=history.next
238
+ self.value = hist
239
+ self.set_insertion_point_end
240
+ return
241
+ else
242
+ self.clear
243
+ end
244
+ end
245
+ evt.skip()
246
+ end
247
+
248
+ # Runs a command through the mock irb class, handles display details.
249
+ def run(cmd)
250
+ (lines = cmd.split(/\r?\n/)).each_index do |idx|
251
+ begin
252
+ line = lines[idx] + "\n"
253
+ @output.default_style = Wx::TextAttr.new(Wx::BLUE)
254
+ @output << ">> #{line}"
255
+
256
+ out, obj = @mirb.run(line)
257
+ @output.default_style = Wx::TextAttr.new(Wx::BLACK)
258
+ @output << out
259
+ @output.default_style = Wx::TextAttr.new(Wx::Colour.new(100,100,100))
260
+ @output << "=> #{obj.inspect}\n"
261
+ rescue MimickIRB::Empty
262
+ rescue MimickIRB::Continue
263
+ if idx == lines.size-1
264
+ @output.default_style = Wx::TextAttr.new(Wx::LIGHT_GREY)
265
+ @output << "...\n"
266
+ end
267
+ rescue Exception => se
268
+ @output.default_style = Wx::TextAttr.new(Wx::RED)
269
+ @output << (se.inspect + "\n" + se.backtrace.join("\n") + "\n")
270
+ end
271
+ end
272
+ end
273
+ end
274
+
275
+
276
+ # The output textbox.
277
+ # This object has a few methods to make it usable (in duck-typing cases) as
278
+ # an IO object. This doesn't actually inherit or implement from the IO
279
+ # class, however.
280
+ class OutputTextCtrl < Wx::TextCtrl
281
+ include Wx
282
+ STYLE = TE_READONLY|TE_MULTILINE|TE_RICH|TE_RICH2|TE_CHARWRAP
283
+
284
+ def initialize(parent)
285
+ super(parent, :style => STYLE)
286
+
287
+ font = Wx::Font.new(10, Wx::MODERN, Wx::NORMAL, Wx::NORMAL)
288
+ set_default_style Wx::TextAttr.new(Wx::BLACK, Wx::WHITE, font)
289
+
290
+ @io_style=Wx::TextAttr.new(Wx::Colour.new(180, 104, 52))
291
+ end
292
+
293
+ #----------------------------------------------------------------------
294
+ # some methods (as i think of/need them) to make it possible to use
295
+ # the output text area as a fake IO object
296
+ #----------------------------------------------------------------------
297
+
298
+ # '<<' outputs to the output window
299
+ alias :<< :append_text
300
+
301
+ # 'close', 'closed?' and 'flush' are also all defined but just emulate
302
+ # an IO object with their return values. Other than that, they do nothing.
303
+ def close ; nil; end
304
+ def closed? ; false ; end
305
+ def flush; self; end
306
+
307
+ # prints directly to the output text area like 'IO.print'
308
+ # and returns nil.
309
+ def print(*args)
310
+ set_default_style @io_style
311
+ self << args.flatten.join
312
+ nil
313
+ end
314
+
315
+ # Displays directly to the output text area like 'IO.puts' and returns nil.
316
+ def puts(*args)
317
+ set_default_style @io_style
318
+ self << args.flatten.map do |o|
319
+ s=o.to_s
320
+ (s[-1,1]=="\n") ? s : s + "\n"
321
+ end.join
322
+ nil
323
+ end
324
+
325
+ # Displays directly to the output text area like 'IO.write' and returns
326
+ # the number of bytes written.
327
+ def write(dat)
328
+ set_default_style @io_style
329
+ out = dat.to_s
330
+ self << out
331
+ out.size
332
+ end
333
+ end
334
+
335
+ # This window object is parent to the Input and Output text areas. It
336
+ # provides a sliding splitter control between the top and bottom text boxes.
337
+ class TerminalSplitter < Wx::SplitterWindow
338
+ def initialize(parent)
339
+ super(parent, :style => Wx::SP_LIVE_UPDATE)
340
+ self.set_sash_gravity(1.0)
341
+ evt_splitter_dclick self, :on_dclick
342
+ end
343
+
344
+ def on_dclick(evt)
345
+ set_sash_position(- @bottom.best_size.height)
346
+ end
347
+
348
+ def split_horizontally(top, bottom, pos=nil)
349
+ @top ||= top
350
+ @bottom ||= bottom
351
+ minsz = @bottom.best_size.height
352
+ set_minimum_pane_size(minsz) # this also prevents unsplitting.
353
+ pos ||= - minsz
354
+ super(@top, @bottom, pos)
355
+ end
356
+ end
357
+
358
+
359
+ # Parent and top-level window for all the wxirb controls.
360
+ class BaseFrame < Wx::Frame
361
+ attr_reader :output, :input
362
+
363
+ def initialize(parent, opts={})
364
+ bind = (opts.delete(:binding) || binding)
365
+ @mirb=MimickIRB.new(bind)
366
+
367
+ opts[:title] ||= "WxIRB"
368
+ super(parent, opts)
369
+
370
+ @splitter = TerminalSplitter.new(self)
371
+ @output = OutputTextCtrl.new(@splitter)
372
+ @input = InputTextCtrl.new(@splitter, @output, @mirb)
373
+ @splitter.split_horizontally(@output, @input)
374
+ @input.set_focus()
375
+ end
376
+
377
+ # Allow our binding to be changed on the fly
378
+ def set_binding(bind); @mirb.set_binding(bind); end
379
+
380
+ # Clears the output window
381
+ def clear; @output.clear ; end
382
+
383
+ # Returns the window's command history object (see WxIRB::CommandHistory)
384
+ def history; @input.history ; end
385
+
386
+ # Prints the history to the output text area. Takes optional start and
387
+ # end position indexes for viewing just a slice of the history array.
388
+ def histdump(s=0, e=-1)
389
+ puts self.history[s..e]
390
+ end
391
+ end
392
+ end
393
+
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: emonti-wxirb
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Eric Monti
8
+ autorequire: wxirb
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-02-13 00:00:00 -08:00
13
+ default_executable: wxirb
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: wxruby
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 1.9.9
24
+ version:
25
+ description:
26
+ email: emonti@matasano.com
27
+ executables:
28
+ - wxirb
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - README.rdoc
33
+ files:
34
+ - README.rdoc
35
+ - bin/wxirb
36
+ - lib/wxirb.rb
37
+ has_rdoc: true
38
+ homepage: http://www.matasano.com
39
+ post_install_message:
40
+ rdoc_options: []
41
+
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: "0"
49
+ version:
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: "0"
55
+ version:
56
+ requirements: []
57
+
58
+ rubyforge_project:
59
+ rubygems_version: 1.2.0
60
+ signing_key:
61
+ specification_version: 2
62
+ summary: A GUI IRB-like console based on WxRuby
63
+ test_files: []
64
+