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