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