1# 2# Converts your TF5 to act (sort of) like TF4. 3# 4# Witness the power of this fully operational death star! This demonstrates 5# just how powerful TinyFugue's triggers are. With enough hooks you can 6# really transform the behavior, though this is admittedly pretty obscene. 7# I just wanted to prove that it was (mostly) possible. 8# 9# What this does is create a fake world then route all input/output through 10# that world. This approach won't work if you're doing anything too fancy with 11# your own macros. Unfortunately because of the way TF handles color we can't 12# do anything with that - text color are hidden attributes, you can't just 13# pass along ascii codes. 14# 15# NOTE: This seems to blow up on Python 2.5.0 (2.5.1 is fine) because it chokes 16# on the key=val argument calls for no reason I can find with a None 17# object error. 18# 19# Usage: 20# /python_load tf4 21# /tf4 help 22# 23# Copyright 2008 Ron Dippold 24# 25# v1.00 - Jan 25 '08 - Initial version 26from lib.py import tf, tfutil 27 28#---------------------------------------------------------- 29# A glorious real world - which we're hiding 30#---------------------------------------------------------- 31class World( object ): 32 33 def __init__( self, name ): 34 self.name = name 35 self.socket = not not name 36 37 global WDICT, WLIST 38 WDICT[self.name.lower()] = self 39 if self.socket: 40 WLIST.append( self ) 41 self.buffer = [] 42 43 # send a line to the world 44 def send( self, text ): 45 if self.socket: 46 tf.send( text, self.name ) 47 48 # called when receiving a line from the world 49 def rx( self, text ): 50 global FG, MODE 51 52 # just accumulate in buffer 53 if MODE == MODE_ON and FG != self: 54 if not self.buffer: 55 tf.out( "%% Activity in world %s" % self.name ) 56 ACTIVITY.append( self ) 57 _activity_status() 58 self.buffer.append( text ) 59 return 60 61 if MODE == MODE_SWITCH: 62 self.activate() 63 64 elif MODE == MODE_MIX: 65 self.check_last() 66 67 tf.out( text ) 68 69 def divider( self ): 70 # show divider 71 if self.name: 72 tf.out( "---- World %s ----" % self.name ) 73 else: 74 tf.out( "---- No world ----" ) 75 76 def check_last( self ): 77 global LAST_OUT 78 if LAST_OUT != self: 79 self.divider() 80 self.dump_buffer() 81 LAST_OUT = self 82 83 # activate this world 84 def activate( self ): 85 self.check_last() 86 87 global FG 88 if self != FG: 89 FG = self 90 tf.eval( "/set _tf4world=%s" % self.name ) 91 92 def dump_buffer( self ): 93 # clean out buffer, no longer active world 94 95 if not self.buffer: 96 return 97 for line in self.buffer: 98 tf.out( line ) 99 self.buffer = [] 100 101 global ACTIVITY 102 if self in ACTIVITY: 103 ACTIVITY.remove( self ) 104 _activity_status() 105 106 def connect( self ): 107 global WLIST 108 self.socket = True 109 if not self in WLIST: 110 WLIST.append( self ) 111 112 self.activate() #??? 113 114 def disconnect( self, reason ): 115 try: 116 WLIST.remove( self ) 117 except ValueError: pass 118 self.socket = False 119 120 121# ----------------------------------------------------------------------------- 122# Global data structures 123# ----------------------------------------------------------------------------- 124 125# Known worlds - in dict format for fast lookup and list format for ordering. 126# Since everything is by reference, this is cheap. 127WDICT = {} 128WLIST = [] 129 130# The world we're pretending is foreground 131FG=World( '' ) 132 133# World we last showed output for 134LAST_OUT=FG 135 136# queue of worlds with activity 137ACTIVITY = [] 138 139# Our current state 140MODE_OFF, MODE_ON, MODE_MIX, MODE_SWITCH = ( 0, 1, 2, 3 ) 141MODE = MODE_OFF 142 143# ------------------------------------------------------------------------------ 144# Helper funcs 145# ------------------------------------------------------------------------------ 146 147# state singleton 148STATE = None 149def getstate(): 150 global STATE 151 if not STATE: 152 STATE = tfutil.State() 153 return STATE 154 155 156def _activity_status(): 157 if ACTIVITY: 158 text = "(Active:%2d)" % len(ACTIVITY) 159 else: 160 text = "" 161 tf.eval( "/set _tf4activity="+text ) 162 163# ------------------------------------------------------------------------------ 164# Our /python_call functions 165# ------------------------------------------------------------------------------ 166 167# Called whenever we get a line from a world 168def rx( argstr ): 169 name, text = argstr.split( ' ', 1 ) 170 world = WDICT.get(name.lower()) 171 if not world: 172 world = World( name ) 173 WDICT[name.lower()] = world 174 world.rx( text ) 175 176# Dispatcher 177def tx( argstr ): 178 FG.send( argstr ) 179 180# Do a /dc - just don't let it /dc our _tf4 world! 181def dc( argstr ): 182 argstr = argstr.lower().strip() 183 184 # do them all 185 if argstr == "-all": 186 for world in WLIST: 187 tf.out( "/@dc %s" % world.name ) 188 tf.eval( "/@dc %s" % world.name ) 189 dischook( world.name + " tf4" ) 190 return 191 192 # otherwise do foreground world 193 if argstr: 194 world = WDICT.get(argstr) 195 else: 196 world = FG 197 198 if world: 199 tf.eval( "/@dc %s" % world.name ) 200 dischook( world.name + " tf4" ) 201 202# change world 203def fg( argstr ): 204 cmd, opts, argstr = tfutil.cmd_parse( argstr, "ns<>c:|q" ) 205 if not opts and not argstr: 206 return 207 208 # no opts, just a world name 209 if not opts: 210 fg = WDICT.get( argstr.lower() ) 211 if fg: 212 fg.activate() 213 else: 214 tf.out( "%% fg: not connected to '%s'" % argstr ) 215 return 216 217 # handle opts 218 fg = None 219 220 # easy, just next activity world 221 if 'a' in opts and ACTIVITY: 222 fg = ACTIVITY[0] 223 fg.activate() 224 return 225 226 # relative movement 227 c = 0 228 if 'c' in opts: 229 try: 230 c = int( opts['c'] ) 231 except: 232 c = 1 233 elif '<' in opts or 'a' in opts: 234 c = -1 235 elif '>' in opts: 236 c = 1 237 238 if c != 0: 239 if WLIST: 240 if FG in WLIST: 241 fg = WLIST[(WLIST.index( FG ) + c) % len( WLIST )] 242 else: 243 fg = WLIST[0] 244 else: 245 fg = WDICT[''] 246 elif 'n' in opts: 247 fg = WDICT[''] 248 249 if fg: 250 fg.activate() 251 252 253# pending hook, just show locally 254def otherhook( argstr ): 255 tf.out( argstr ) 256 257# world connected hook 258def conhook( argstr ): 259 name = argstr.split()[0] 260 world = WDICT.get( name.lower() ) 261 if not world: 262 # create a new world - adds itself to WDICT, WLIST 263 world = World( name ) 264 265 tf.eval( "/@fg _tf4" ) # just in case 266 world.connect() 267 268 269# world disconnected hook 270def dischook( argstr ): 271 split = argstr.split(None,1) 272 name = split[0].lower() 273 reason = len(split)>1 and split[1] or '' 274 if name in WDICT: 275 WDICT[name].disconnect( reason ) 276 if reason != 'tf4': 277 tf.out( "%% Connection to %s closed." % name ) 278 world( "-a" ) 279 280# connection request - just make sure we start in background 281def connect( argstr ): 282 cmd, opts, argstr = tfutil.cmd_parse( argstr, "lqxfb" ) 283 opts['b'] = '' 284 if 'f' in opts: 285 del opts['f'] 286 287 tf.out( tfutil.cmd_unparse( "/@connect", opts, argstr ) ) 288 tf.eval( tfutil.cmd_unparse( "/@connect", opts, argstr ) ) 289 290# world is a hybrid of connect and fg 291def world( argstr ): 292 cmd, opts, argstr = tfutil.cmd_parse( argstr, "lqxfb" ) 293 name = argstr.lower() 294 if name and name in WDICT: 295 name.activate() 296 elif opts: 297 fg( argstr ) 298 else: 299 connect( argstr ) 300 301# Just make sure we have a specific world 302def recall( argstr ): 303 cmd, opts, argstr = tfutil.cmd_parse( argstr, 'w:ligvt:a:m:A:B:C:' ) 304 305 if not 'w' in opts or not opts['w']: 306 opts['w'] = FG.name 307 308 tf.eval( tfutil.cmd_unparse( "/@recall", opts, argstr ) ) 309 310def quit( argstr ): 311 cmd, opts, argstr = tfutil.cmd_parse( argstr, 'y' ) 312 313 if not ACTIVITY: 314 opts['y'] = '' 315 316 tf.eval( tfutil.cmd_unparse( "/@quit", opts, argstr ) ) 317 318# ---------------------------------------------------------------------------- 319# Initial setup 320# ---------------------------------------------------------------------------- 321 322def myhelp(): 323 for line in """ 324/tf4 (ON|mix|switch|off) 325 326on: Turns on TinyFugue 4 emulation mode where all world activity is shown 327 in a single world separated by ---- World Foo ---- markers. This is 328 emulated with hooks and scripts, so if your own scripts get too fancy 329 it will break, but it seems to work pretty well with mine. 330 331mix: Like 'on', but output in background worlds will be immediately shown 332 (with a divider) so you can just watch the output of all worlds scroll 333 by. However the world does not become the foreground world! Without 334 /visual on you won't really have any indication of what the foreground 335 world is so you won't know where you're sending text. 336 337switch: Like 'mix' but immediately switches you to whatever world has output. 338 Obviously this makes it tough to guarantee that any text you're sending 339 will go to the right world unless you do a /send -wfoo text, since it 340 could switch just before you hit enter. 341 342off: Turn off TinyFugue 4 emulation mode, revert all your keybindings and 343 macro definitions back to the way they were. 344""".split("\n"): 345 tf.out( line ) 346 347def tf4( argstr ): 348 349 # Create a new base state if we don't have one 350 state = getstate() 351 352 353 # Just parse the command 354 global MODE 355 argstr = argstr.lower() 356 if not argstr or 'on' in argstr: 357 newmode = MODE_ON 358 359 elif 'help' in argstr: 360 return myhelp() 361 362 elif 'off' in argstr: 363 newmode = MODE_OFF 364 365 elif 'mix' in argstr: 366 newmode = MODE_MIX 367 368 elif 'switch' in argstr: 369 newmode = MODE_SWITCH 370 371 else: 372 return myhelp() 373 374 # Now do what we need to 375 if newmode == MODE_OFF: 376 if newmode != MODE: 377 state.revert() 378 tf.eval( "/dc _tf4" ) 379 tf.eval( "/unworld _tf4" ) 380 sockets = tfutil.listsockets() 381 for name in sockets: 382 tf.eval( "/@fg -q " + name ) 383 MODE = MODE_OFF 384 tf.out( "% tf4 mode is now off. '/tf4' to re-enable it" ) 385 return 386 387 if MODE == MODE_OFF: 388 # Create a virtual world to hold our text 389 sockets = tfutil.listsockets() 390 if not "_tf4" in sockets: 391 tf.eval( "/addworld _tf4" ) 392 tf.eval( "/connect _tf4" ) 393 394 # We want background processing and triggers 395 state.setvar( 'background', 'on' ) 396 state.setvar( 'bg_output', 'off' ) 397 state.setvar( 'borg', 'on' ) 398 state.setvar( 'gag', 'on' ) 399 state.setvar( 'hook', 'on' ) 400 401 # change the status bar 402 state.setvar( '_tf4world', tf.eval( '/test fg_world()' ) ) 403 state.setvar( '_tf4activity', '' ) 404 state.status( "rm @world" ) 405 state.status( "add -A@more _tf4world" ) 406 state.status( "rm @active" ) 407 state.status( "add -A@read _tf4activity:11" ) 408 409 # Grab text from any world except our dummy world - we use a regexp instead of a glob 410 # because the glob does not preserve the spacing 411 state.define( flags="-p99999 -w_tf4 -ag -mglob -t*" ) 412 state.define( body="/python_call tf4.rx $[world_info()] %P1", 413 flags="-Fpmaxpri -q -mregexp -t(.*)" ) 414 415 # add a trigger for anything being sent to the generic world, so we can reroute 416 # it to the right world 417 state.define( body="/python_call tf4.tx %{*}", flags="-pmaxpri -w_tf4 -hSEND" ) 418 419 # Add our hooks 420 state.define( body="/python_call tf4.dischook %{*}", 421 flags="-Fpmaxpri -ag -msimple -hDISCONNECT" ) 422 state.define( body="/python_call tf4.conhook %{*}", 423 flags="-Fpmaxpri -ag -msimple -hCONNECT" ) 424 state.define( body="/python_call tf4.otherhook %{*}", 425 flags='-p999 -ag -msimple -hCONFAIL|ICONFAIL|PENDING|DISCONNECT' ) 426 state.define( body="", flags="-pmaxpri -ag -msimple -hBGTRIG" ) 427 state.define( body="", flags="-pmaxpri -ag -msimple -hACTIVITY" ) 428 429 # Bind Esc-b, Esc-f to call us instead 430 state.bind( key="^[b", body="/python_call tf4.fg -<", flags="i" ) 431 state.bind( key="^[f", body="/python_call tf4.fg ->", flags="i" ) 432 state.bind( key="^[w", body="/python_call tf4.fg -a", flags="i" ) 433 434 # def these commands to go to us 435 state.define( "bg", "/python_call tf4.fg -n" ) 436 state.define( "connect", "/python_call tf4.connect %{*}" ) 437 state.define( "dc", "/python_call tf4.dc" ) 438 state.define( "fg", "/python_call tf4.fg %{*}" ) 439 state.define( "quit", "/python_call tf4.quit ${*}" ) 440 state.define( "recall", "/python_call tf4.recall %{*}" ) 441 state.define( "to_active_or_prev_world", "/python_call tf4.fg -a" ) 442 state.define( "to_active_world", "/python_call tf4.fg -a" ) # close enough 443 state.define( "world", "/python_call tf4.world %{*}" ) 444 445 MODE = newmode 446 if newmode == MODE_SWITCH: 447 tf.out( "% TinyFugue 4 emulation mode with autoswitch to output world." ) 448 elif newmode == MODE_MIX: 449 tf.out( "% TinyFugue 4 emulation mode with background output shown." ) 450 else: 451 tf.out( "% TinyFugue 4 emulation mode is on." ) 452 tf.out( "% '/tf4 off' to disable, '/tf4 help' for help." ) 453 454# ------------------------------------- 455# When first loaded 456# ------------------------------------- 457 458# register ourself 459state = getstate() 460state.define( name="tf4", body="/python_call tf4.tf4 %*", flags="-q" ) 461tf.out( "% '/tf4 help' for how to invoke TinyFugue 4 emulation mode." ) 462