1#!/usr/bin/env ruby
2
3# rmt --
4# This script implements a simple remote-control mechanism for
5# Tk applications.  It allows you to select an application and
6# then type commands to that application.
7
8require 'tk'
9
10class Rmt
11  def initialize(parent=nil)
12    win = self
13
14    unless parent
15      parent = TkRoot.new
16    end
17    root = TkWinfo.toplevel(parent)
18    root.minsize(1,1)
19
20    # The instance variable below keeps track of the remote application
21    # that we're sending to.  If it's an empty string then we execute
22    # the commands locally.
23    @app = 'local'
24    @mode = 'Ruby'
25
26    # The instance variable below keeps track of whether we're in the
27    # middle of executing a command entered via the text.
28    @executing = 0
29
30    # The instance variable below keeps track of the last command executed,
31    # so it can be re-executed in response to !! commands.
32    @lastCommand = ""
33
34    # Create menu bar.  Arrange to recreate all the information in the
35    # applications sub-menu whenever it is cascaded to.
36
37    TkFrame.new(root, 'relief'=>'raised', 'bd'=>2) {|f|
38      pack('side'=>'top', 'fill'=>'x')
39      TkMenubutton.new(f, 'text'=>'File', 'underline'=>0) {|mb|
40	TkMenu.new(mb) {|mf|
41	  mb.menu(mf)
42	  TkMenu.new(mf) {|ma|
43	    postcommand proc{win.fillAppsMenu ma}
44	    mf.add('cascade', 'label'=>'Select Application',
45		   'menu'=>ma, 'underline'=>0)
46	  }
47	  add('command', 'label'=>'Quit',
48	      'command'=>proc{root.destroy}, 'underline'=>0)
49	}
50	pack('side'=>'left')
51      }
52    }
53
54    # Create text window and scrollbar.
55
56    @txt = TkText.new(root, 'relief'=>'sunken', 'bd'=>2, 'setgrid'=>true) {
57      yscrollbar(TkScrollbar.new(root){pack('side'=>'right', 'fill'=>'y')})
58      pack('side'=>'left')
59    }
60
61    @promptEnd = TkTextMark.new(@txt, 'insert')
62
63    # Create a binding to forward commands to the target application,
64    # plus modify many of the built-in bindings so that only information
65    # in the current command can be deleted (can still set the cursor
66    # earlier in the text and select and insert;  just can't delete).
67
68    @txt.bindtags([@txt, TkText, root, 'all'])
69    @txt.bind('Return', proc{
70		@txt.set_insert('end - 1c')
71		@txt.insert('insert', "\n")
72		win.invoke
73		Tk.callback_break
74	      })
75    @txt.bind('Delete', proc{
76		begin
77		  @txt.tag_remove('sel', 'sel.first', @promptEnd)
78		rescue
79		end
80		if @txt.tag_nextrange('sel', '1.0', 'end') == []
81		  if @txt.compare('insert', '<', @promptEnd)
82		    Tk.callback_break
83		  end
84		end
85	      })
86    @txt.bind('BackSpace', proc{
87		begin
88		  @txt.tag_remove('sel', 'sel.first', @promptEnd)
89		rescue
90		end
91		if @txt.tag_nextrange('sel', '1.0', 'end') == []
92		  if @txt.compare('insert', '<', @promptEnd)
93		    Tk.callback_break
94		  end
95		end
96	      })
97    @txt.bind('Control-d', proc{
98		if @txt.compare('insert', '<', @promptEnd)
99		  Tk.callback_break
100		end
101	      })
102    @txt.bind('Control-k', proc{
103		if @txt.compare('insert', '<', @promptEnd)
104		  @txt.set_insert(@promptEnd)
105		end
106	      })
107    @txt.bind('Control-t', proc{
108		if @txt.compare('insert', '<', @promptEnd)
109		  Tk.callback_break
110		end
111	      })
112    @txt.bind('Meta-d', proc{
113		if @txt.compare('insert', '<', @promptEnd)
114		  Tk.callback_break
115		end
116	      })
117    @txt.bind('Meta-BackSpace', proc{
118		if @txt.compare('insert', '<=', @promptEnd)
119		  Tk.callback_break
120		end
121	      })
122    @txt.bind('Control-h', proc{
123		if @txt.compare('insert', '<=', @promptEnd)
124		  Tk.callback_break
125		end
126	      })
127
128    @txt.tag_configure('bold', 'font'=>['Courier', 12, 'bold'])
129
130    @app = Tk.appname('rmt')
131    if (@app =~ /^rmt(.*)$/)
132      root.title("Tk Remote Controller#{$1}")
133      root.iconname("Tk Remote#{$1}")
134    end
135    prompt
136    @txt.focus
137    #@app = TkWinfo.appname(TkRoot.new)
138  end
139
140  def tkTextInsert(w,s)
141    return if s == ""
142    begin
143      if w.compare('sel.first','<=','insert') \
144	&& w.compare('sel.last','>=','insert')
145	w.tag_remove('sel', 'sel.first', @promptEnd)
146	w.delete('sel.first', 'sel.last')
147      end
148    rescue
149    end
150    w.insert('insert', s)
151    w.see('insert')
152  end
153
154  # The method below is used to print out a prompt at the
155  # insertion point (which should be at the beginning of a line
156  # right now).
157
158  def prompt
159    @txt.insert('insert', "#{@app}: ")
160    @promptEnd.set('insert')
161    @promptEnd.gravity = 'left'
162    @txt.tag_add('bold', "#{@promptEnd.path} linestart", @promptEnd)
163  end
164
165  # The method below executes a command (it takes everything on the
166  # current line after the prompt and either sends it to the remote
167  # application or executes it locally, depending on "app".
168
169  def invoke
170    cmd = @txt.get(@promptEnd, 'insert')
171    @executing += 1
172    case (@mode)
173    when 'Tcl'
174      if Tk.info('complete', cmd)
175	if (cmd == "!!\n")
176	  cmd = @lastCommand
177	else
178	  @lastCommand = cmd
179	end
180	begin
181	  msg = Tk.appsend(@app, false, cmd)
182	rescue
183	  msg = "Error: #{$!}"
184	end
185	@txt.insert('insert', msg + "\n") if msg != ""
186	prompt
187	@promptEnd.set('insert')
188      end
189
190    when 'Ruby'
191      if (cmd == "!!\n")
192	cmd = @lastCommand
193      end
194      complete = true
195      begin
196	eval("proc{#{cmd}}")
197      rescue
198	complete = false
199      end
200      if complete
201	@lastCommand = cmd
202	begin
203#	  msg = Tk.appsend(@app, false,
204#			   'ruby',
205#			   '"(' + cmd.gsub(/[][$"]/, '\\\\\&') + ').to_s"')
206	  msg = Tk.rb_appsend(@app, false, cmd)
207	rescue
208	  msg = "Error: #{$!}"
209	end
210	@txt.insert('insert', msg + "\n") if msg != ""
211	prompt
212	@promptEnd.set('insert')
213      end
214    end
215
216    @executing -= 1
217    @txt.yview_pickplace('insert')
218  end
219
220  # The following method is invoked to change the application that
221  # we're talking to.  It also updates the prompt for the current
222  # command, unless we're in the middle of executing a command from
223  # the text item (in which case a new prompt is about to be output
224  # so there's no need to change the old one).
225
226  def newApp(appName, mode)
227    @app = appName
228    @mode = mode
229    if @executing == 0
230      @promptEnd.gravity = 'right'
231      @txt.delete("#{@promptEnd.path} linestart", @promptEnd)
232      @txt.insert(@promptEnd, "#{appName}: ")
233      @txt.tag_add('bold', "#{@promptEnd.path} linestart", @promptEnd)
234      @promptEnd.gravity = 'left'
235    end
236  end
237
238  # The method below will fill in the applications sub-menu with a list
239  # of all the applications that currently exist.
240
241  def fillAppsMenu(menu)
242    win = self
243    begin
244      menu.delete(0,'last')
245    rescue
246    end
247    TkWinfo.interps.sort.each{|ip|
248      begin
249	if Tk.appsend(ip, false, 'info commands ruby') == ""
250	  mode = 'Tcl'
251	else
252	  mode = 'Ruby'
253	end
254	menu.add('command', 'label'=>format("%s    (#{mode}/Tk)", ip),
255		 'command'=>proc{win.newApp ip, mode})
256      rescue
257	menu.add('command', 'label'=>format("%s (unknown Tk)", ip),
258		 'command'=>proc{win.newApp ip, mode}, 'state'=>'disabled')
259      end
260    }
261    menu.add('command', 'label'=>format("local    (Ruby/Tk)"),
262	     'command'=>proc{win.newApp 'local', 'Ruby'})
263  end
264end
265
266Rmt.new
267
268Tk.mainloop
269