#!/usr/bin/env ruby # rmt -- # This script implements a simple remote-control mechanism for # Tk applications. It allows you to select an application and # then type commands to that application. require 'tk' class Rmt def initialize(parent=nil) win = self unless parent parent = TkRoot.new end root = TkWinfo.toplevel(parent) root.minsize(1,1) # The instance variable below keeps track of the remote application # that we're sending to. If it's an empty string then we execute # the commands locally. @app = 'local' @mode = 'Ruby' # The instance variable below keeps track of whether we're in the # middle of executing a command entered via the text. @executing = 0 # The instance variable below keeps track of the last command executed, # so it can be re-executed in response to !! commands. @lastCommand = "" # Create menu bar. Arrange to recreate all the information in the # applications sub-menu whenever it is cascaded to. TkFrame.new(root, 'relief'=>'raised', 'bd'=>2) {|f| pack('side'=>'top', 'fill'=>'x') TkMenubutton.new(f, 'text'=>'File', 'underline'=>0) {|mb| TkMenu.new(mb) {|mf| mb.menu(mf) TkMenu.new(mf) {|ma| postcommand proc{win.fillAppsMenu ma} mf.add('cascade', 'label'=>'Select Application', 'menu'=>ma, 'underline'=>0) } add('command', 'label'=>'Quit', 'command'=>proc{root.destroy}, 'underline'=>0) } pack('side'=>'left') } } # Create text window and scrollbar. @txt = TkText.new(root, 'relief'=>'sunken', 'bd'=>2, 'setgrid'=>true) { yscrollbar(TkScrollbar.new(root){pack('side'=>'right', 'fill'=>'y')}) pack('side'=>'left') } @promptEnd = TkTextMark.new(@txt, 'insert') # Create a binding to forward commands to the target application, # plus modify many of the built-in bindings so that only information # in the current command can be deleted (can still set the cursor # earlier in the text and select and insert; just can't delete). @txt.bindtags([@txt, TkText, root, 'all']) @txt.bind('Return', proc{ @txt.set_insert('end - 1c') @txt.insert('insert', "\n") win.invoke Tk.callback_break }) @txt.bind('Delete', proc{ begin @txt.tag_remove('sel', 'sel.first', @promptEnd) rescue end if @txt.tag_nextrange('sel', '1.0', 'end') == [] if @txt.compare('insert', '<', @promptEnd) Tk.callback_break end end }) @txt.bind('BackSpace', proc{ begin @txt.tag_remove('sel', 'sel.first', @promptEnd) rescue end if @txt.tag_nextrange('sel', '1.0', 'end') == [] if @txt.compare('insert', '<', @promptEnd) Tk.callback_break end end }) @txt.bind('Control-d', proc{ if @txt.compare('insert', '<', @promptEnd) Tk.callback_break end }) @txt.bind('Control-k', proc{ if @txt.compare('insert', '<', @promptEnd) @txt.set_insert(@promptEnd) end }) @txt.bind('Control-t', proc{ if @txt.compare('insert', '<', @promptEnd) Tk.callback_break end }) @txt.bind('Meta-d', proc{ if @txt.compare('insert', '<', @promptEnd) Tk.callback_break end }) @txt.bind('Meta-BackSpace', proc{ if @txt.compare('insert', '<=', @promptEnd) Tk.callback_break end }) @txt.bind('Control-h', proc{ if @txt.compare('insert', '<=', @promptEnd) Tk.callback_break end }) @txt.tag_configure('bold', 'font'=>['Courier', 12, 'bold']) @app = Tk.appname('rmt') if (@app =~ /^rmt(.*)$/) root.title("Tk Remote Controller#{$1}") root.iconname("Tk Remote#{$1}") end prompt @txt.focus #@app = TkWinfo.appname(TkRoot.new) end def tkTextInsert(w,s) return if s == "" begin if w.compare('sel.first','<=','insert') \ && w.compare('sel.last','>=','insert') w.tag_remove('sel', 'sel.first', @promptEnd) w.delete('sel.first', 'sel.last') end rescue end w.insert('insert', s) w.see('insert') end # The method below is used to print out a prompt at the # insertion point (which should be at the beginning of a line # right now). def prompt @txt.insert('insert', "#{@app}: ") @promptEnd.set('insert') @promptEnd.gravity = 'left' @txt.tag_add('bold', "#{@promptEnd.path} linestart", @promptEnd) end # The method below executes a command (it takes everything on the # current line after the prompt and either sends it to the remote # application or executes it locally, depending on "app". def invoke cmd = @txt.get(@promptEnd, 'insert') @executing += 1 case (@mode) when 'Tcl' if Tk.info('complete', cmd) if (cmd == "!!\n") cmd = @lastCommand else @lastCommand = cmd end begin msg = Tk.appsend(@app, false, cmd) rescue msg = "Error: #{$!}" end @txt.insert('insert', msg + "\n") if msg != "" prompt @promptEnd.set('insert') end when 'Ruby' if (cmd == "!!\n") cmd = @lastCommand end complete = true begin eval("proc{#{cmd}}") rescue complete = false end if complete @lastCommand = cmd begin # msg = Tk.appsend(@app, false, # 'ruby', # '"(' + cmd.gsub(/[][$"]/, '\\\\\&') + ').to_s"') msg = Tk.rb_appsend(@app, false, cmd) rescue msg = "Error: #{$!}" end @txt.insert('insert', msg + "\n") if msg != "" prompt @promptEnd.set('insert') end end @executing -= 1 @txt.yview_pickplace('insert') end # The following method is invoked to change the application that # we're talking to. It also updates the prompt for the current # command, unless we're in the middle of executing a command from # the text item (in which case a new prompt is about to be output # so there's no need to change the old one). def newApp(appName, mode) @app = appName @mode = mode if @executing == 0 @promptEnd.gravity = 'right' @txt.delete("#{@promptEnd.path} linestart", @promptEnd) @txt.insert(@promptEnd, "#{appName}: ") @txt.tag_add('bold', "#{@promptEnd.path} linestart", @promptEnd) @promptEnd.gravity = 'left' end end # The method below will fill in the applications sub-menu with a list # of all the applications that currently exist. def fillAppsMenu(menu) win = self begin menu.delete(0,'last') rescue end TkWinfo.interps.sort.each{|ip| begin if Tk.appsend(ip, false, 'info commands ruby') == "" mode = 'Tcl' else mode = 'Ruby' end menu.add('command', 'label'=>format("%s (#{mode}/Tk)", ip), 'command'=>proc{win.newApp ip, mode}) rescue menu.add('command', 'label'=>format("%s (unknown Tk)", ip), 'command'=>proc{win.newApp ip, mode}, 'state'=>'disabled') end } menu.add('command', 'label'=>format("local (Ruby/Tk)"), 'command'=>proc{win.newApp 'local', 'Ruby'}) end end Rmt.new Tk.mainloop