#
# config.py
#
# This attempts to manage your TinyFugue configuration, letting you select,
# edit, and save your worlds using /commands. Will expand this to key bindings
# later. This is my first curses program, so it's pretty uuuuugly, sorry.
#
# Usage:
# /python_load config
# /python_call config.worldsfile ~/.tf/worlds (optional)
# /worlds
#
# If you don't do the config.worldsfile() to tell it where your worlds are
# located it will attempt to figure it out by looking for a /loadworlds in
# your .tfrc. Or if it finds addworld commands in your .tfrc it will assume
# it should just dump them in there.
#
#
# Copyright 2008 Ron Dippold sizer@san.rr.com
#
# v1.01 - Jan 23 '08 - Convert to using tfutil
# v1.00 - Jan 20 '08 - First version
#
import curses, os
import tf, tfutil
# ------------------------------------------------------
# Where's the worldsfile?
# ------------------------------------------------------
WORLDSFILE=None
def worldsfile( fname ):
global WORLDSFILE
WORLDSFILE=fname
def _find_worldsfile():
# already found
global WORLDSFILE
if WORLDSFILE:
return WORLDSFILE
# look in tfrc
tfrc = tf.tfrc()
if not tfrc:
return None
# search through tfrc
f = open( tfrc, "rU" )
for line in f:
if line.startswith( "/loadworld " ):
WORLDSFILE=line.split()[1].strip()
break
if "addworld" in line:
WORLDSFILE = tfrc
break
f.close()
if WORLDSFILE:
WORLDSFILE = os.path.abspath( os.path.expanduser( WORLDSFILE ) )
return WORLDSFILE
# ------------------------------------------------------
# helpers
# ------------------------------------------------------
# show help line with [f]oo as foo
def _showkeys( window, y, x, fields ):
maxy, maxx = window.getmaxyx()
window.addnstr( y, x, " "*132, maxx-x-2 )
window.addstr( y, x, fields.replace('[','').replace(']','') )
offset, pos= 0, 0
while True:
pos = fields.find( '[', pos )
if pos<0: break
offset += 1
end = fields.find( ']', pos )
if end<0: break
window.addstr( y, x+pos-offset+1, fields[pos+1:end], curses.A_BOLD )
pos = end
offset += 1
# -----------------------------------------------------------------------
# Edit the worlds
# -----------------------------------------------------------------------
def worlds( argstr ):
# wrap the function in case it crashes
curses.wrapper( _worlds )
# screen is all messed up, redraw it
tf.eval( "/dokey REDRAW" )
def _new_undo( undo, wdict, message, saved ):
undo.append( ( message, dict( [ ( key, tfutil.World(val) ) \
for key, val in wdict.items() ] ), saved ) )
SAVED = True
def _change_worlds( old, wdict ):
# remove old worlds
for name, item in old.items():
if name not in wdict:
tf.eval( "/unworld " + name )
# add new or changed worlds
for name, item in wdict.items():
cmd = item.addworld_command( func=True, full=True )
if name not in old:
if cmd: tf.eval( cmd )
elif item.changed_from(old[name]):
tf.eval( "/unworld " + name )
if cmd: tf.eval( cmd )
def _save_worlds2( fout, wdict ):
for name, item in sorted(wdict.items()):
cmd = item.addworld_command( func=False, full=True )
if cmd:
fout.write( cmd + '\n' )
return True
def _save_worlds( wdict ):
fname = _find_worldsfile()
if not fname:
return False, "Can't find world file. Use '/python_call config.worldsfile '"
# Read the old lines except for addworld, write to new file
try:
fout = open( fname+".xxx", "wt", 0600 )
except IOError, e:
return False, "Error opening %s for write: %s" % ( e.filename, e.strerror )
fin = open( fname, "rU" )
written = False
for line in fin:
if "addworld" in line:
if not written:
written = _save_worlds2( fout, wdict )
continue
# write, converting line endings
fout.write( line )
fin.close()
# write all the worlds to the end of the file if haven't done it yet
if not written:
written = _save_worlds2( fout, wdict )
fout.close()
# move the new file over the old one
os.rename( fin.name, fin.name+".bak" )
os.rename( fout.name, fin.name )
tf.out( "- saved /worlds to %s" % fin.name )
return True, None
# This is the real worlds function, invoked by curses safety wrapper
def _worlds( stdscr ):
# get dictionary of current worlds
wdict = tfutil.listworlds( asdict=True )
cols, lines = tfutil.screensize()
colors = curses.can_change_color()
# Draw the border and create a world window
stdscr.clear()
stdscr.border()
stdscr.addstr( 0, 4, "| TinyFugue Worlds |", curses.A_BOLD )
wpos, lastwpos = 0, 9999
worldwin, scrollwin = _worldwin( stdscr, lines-4, cols-4, 2, 2 )
#worldwin, scrollwin = _worldwin( stdscr, 15, cols-4, 2, 2 )
worldwin.refresh()
# start an undo stack
undo, redo = [], []
global SAVED
# now loop
message, lastmessage = None, "dummy"
while True:
# sort by name
wlist = sorted( wdict.values() )
# draw the list, get a command
_drawworlds( wlist, scrollwin, wpos, lastwpos )
lastwpos = wpos
#
# parse keys
#
c = scrollwin.getkey()
# Movement
if c in ( 'i', 'KEY_UP', '\x10' ):
if wpos>0:
wpos -= 1
elif c in ( 'j', 'KEY_DOWN', '\x0e' ) :
if (wpos+1) < len( wlist ):
wpos += 1
# actual editing
elif wlist and c in ( 'd', 'KEY_DC' ):
message = 'Deleted world %s' % wlist[wpos].name
_new_undo( undo, wdict, message, SAVED )
del wdict[ wlist[wpos].name ]
wpos = min( wpos, len(wdict)-1 )
SAVED, lastwpos = False, 9999
elif wlist and c in ( 'c', ):
w = wlist[wpos]
for i in range( 2, 99 ):
newname = '%s(%d)' % ( w.name, i )
if not newname in wdict:
message = 'Copied world %s' % newname
_new_undo( undo, wdict, message, SAVED )
newworld = tfutil.World(w)
newworld.name = newname
wdict[newname] = newworld
SAVED, lastwpos = False, 9999
break
elif c in ( 'a', 'KEY_INS' ):
w = _editworld( worldwin, wdict.keys(), None )
if w:
message = 'Added world %s' % w.name
_new_undo( undo, wdict, message, SAVED )
wdict[ w.name ] = w
SAVED = False
lastwpos = 9999
_worldwin_redraw( worldwin )
elif wlist and c in ( 'e', ):
oldname = wlist[wpos].name
w = _editworld( worldwin, wdict.keys(), tfutil.World(wlist[wpos]) )
if w:
message = 'Edited world %s' % w.name
_new_undo( undo, wdict, message, SAVED )
if oldname!=w.name:
del wdict[oldname]
wdict[ w.name ] = w
SAVED = False
lastwpos = 9999
_worldwin_redraw( worldwin )
# undo/redo
elif c == 'u':
if undo:
message, wdict2, SAVED2 = undo.pop()
_new_undo( redo, wdict, message, SAVED )
wdict, SAVED = wdict2, SAVED2
message = "Undid: " + message
lastwpos = 9999
else:
message = 'Nothing to undo'
elif c == 'r':
if redo:
message, wdict2, SAVED2 = redo.pop()
_new_undo( undo, wdict, message, SAVED )
wdict, SAVED = wdict2, SAVED2
message = "Redid: " + message
lastwpos = 9999
else:
message = 'Nothing to redo'
# Anything that terminates us
elif wlist and c in ( '\n', 'KEY_ENTER', 'Q' ):
if undo:
_change_worlds( undo[0][1], wdict )
if c != 'Q':
tf.eval( "/connect %s" % wlist[wpos].name )
if not SAVED:
tf.err( "* Warning: your /worlds haven't been saved yet" )
break
elif c == 'S':
if undo:
_change_worlds( undo[0][1], wdict )
if not SAVED:
SAVED, message = _save_worlds( wdict )
if SAVED:
break
elif c == 'A':
if not SAVED:
tf.err( "* all /worlds changes aborted" )
SAVED = False
break
message, lastmessage = _show_message( stdscr, message, lastmessage )
def _show_message( window, message, lastmessage ):
y, x = window.getmaxyx()
if message:
window.addnstr( 1, 1, " " + message + " "*x, x-2, curses.A_REVERSE )
lastmessage = message
window.refresh()
elif lastmessage:
window.addnstr( 1, 1, " "*x, x-2, curses.A_NORMAL )
window.refresh()
lastmessage = None
return None, lastmessage
def _worldwin_redraw( window ):
lines, cols = window.getmaxyx()
window.erase()
# title
window.addnstr( 0, 0, "World Character Host Port",
cols, curses.A_BOLD )
# help line
_showkeys( window, lines-1, 0,
'[enter] - [a]dd [c]opy [d]el [e]dit - [u]ndo/[r]edo - [S]ave [Q]uit [A]bort' )
def _worldwin( parent, lines, cols, y, x ):
lines -= 4
cols -= 4
window = parent.derwin( lines, cols, 2, 2 )
window.keypad(1)
_worldwin_redraw( window )
scrollwin = window.derwin( lines-2, cols, 1, 0 )
scrollwin.keypad(1)
scrollwin.erase()
return window, scrollwin
def _drawworlds( wlist, window, wpos, lastwpos ):
# figure out the top world we're showing
lines, cols = window.getmaxyx()
top = wpos - lines + 1
if top<0: top = 0
lasttop = lastwpos - lines + 1
if lasttop<0: lasttop=0
# Try to avoid redrawing the whole thing
start, end = min( lastwpos, wpos ), max( lastwpos, wpos )+1
if top == lasttop:
pass
elif top == (lasttop+1):
window.move( 0, 0 )
window.deleteln()
else:
start, end = 0, len( wlist )
# list each visible world
for y in range( lines ):
window.move( y, 0 )
i = top + y
if i >= len( wlist ):
window.clrtoeol()
else:
if i=end:
continue
attrib = (wpos == i) and curses.A_REVERSE or curses.A_NORMAL
item = wlist[i]
ssl = ( 'x' in item.flags ) and '*' or ' '
window.addnstr( "%-10s %-16s %s%-20s %5s" % \
( item.name, item.character, ssl, item.host, item.port ),
cols, attrib )
window.move( wpos-top, 0 )
window.refresh()
# Define our functions
tf.eval( "/def worlds=/python_call config.worlds" )
# ---------------------------------------------------------------------------
# Editing a world
# ---------------------------------------------------------------------------
def _validate_port( world, worldnames, value ):
try:
if int(value)>0 and int(value)<65536:
return None
except:
pass
return "Port number must be from 1 and 65535."
def _validate_name( world, worldnames, value ):
if not value:
return "The world name may not be blank."
if value in worldnames:
return "There is already a world with that name."
return None
# Required:
# 'name' : label
# 'help' : help field
# Either 'key' or 'get'/'set' must be set:
# 'key' : name of dict key
# 'get' : function that returns value from ( world )
# 'set' : function that sets val in world from ( world, val )
# Optional:
# 'edit' : function to edit val from ( world, val ) if you don't want textpad
# 'validate': function passed ( world, worldnames, value ) and returns error
# message, or None if it passes validation
EDITFIELDS = [
{ 'name': 'World',
'help': 'Name of the world, used for /world',
'key': 'name',
'validate': _validate_name,
},
{ 'name': 'Type',
'help': 'Mu* type: aber, diku, lp, lpp, telnet, tiny',
'key': 'type',
},
{ 'name': 'Host',
'help': 'Host name or IP address',
'key': 'host',
},
{ 'name': 'Port',
'help': 'Port number - 1 to 65535',
'key': 'port',
'validate': _validate_port,
},
{ 'name': 'Character',
'help': 'Character name for auto-logon',
'key': 'character',
},
{ 'name': 'Password',
'help': 'Character password for auto-logon',
'key': 'password',
},
{ 'name': 'Use SSL',
'help': 'Use SSL to connect to the world (if compiled in)',
'yesno': True,
'edit': lambda w,v: v == 'Yes' and 'No' or 'Yes',
'get': lambda w: ( 'x' in w.flags ) and 'Yes' or 'No',
'set': lambda w,v: _setflag( w, 'x', v ),
},
{ 'name': 'Ignore Proxy',
'help': 'ignore %{proxy_host} if set',
'yesno': True,
'edit': lambda w,v: v == 'Yes' and 'No' or 'Yes',
'get': lambda w: ( 'p' in w.flags ) and 'Yes' or 'No',
'set': lambda w,v: _setflag( w, 'p', v ),
},
{ 'name': 'Echo input',
'help': 'Echo back all text sent to the world',
'yesno': True,
'edit': lambda w,v: v == 'Yes' and 'No' or 'Yes',
'get': lambda w: ( 'e' in w.flags ) and 'Yes' or 'No',
'set': lambda w,v: _setflag( w, 'e', v ),
},
{ 'name': 'Macro file',
'help': 'Macro file to /load on connect',
'key': 'file',
},
{ 'name': 'Src Host',
'help': 'Set source IP to bind to a specific interface',
'key': 'srchost',
},
]
def _setflag( world, flag, val ):
add = val.lower().startswith("y") and flag or ""
world.flags = world.flags.replace(flag,'') + add
def _getfield( world, field ):
if 'get' in field:
return field['get'](world)
else:
return getattr( world, field['key'] )
def _setfield( world, field, value ):
if 'set' in field:
field['set'](world,value)
else:
setattr( world, field['key'], value )
def _editworld( worldwin, worldnames, world ):
import curses.textpad
# if the world is empty, create a default world
if not world:
world = World()
else:
worldnames.remove( world.name )
# create a new window as a subwindow of the old
lines0, cols0 = worldwin.getmaxyx()
lines = min( lines0-4, 17 )
cols = cols0-4
y = (lines0-lines)/2
editwin = worldwin.derwin( lines, cols, y, 2 )
editwin.keypad( 1 )
editwin.erase()
editwin.border()
editwin.addstr( 0, 4, "| Editing World: %s |" % (world.name and world.name or ''), \
curses.A_BOLD )
_showkeys( editwin, lines-2, 2,
'[d]elete [enter]/[e]dit - [Q]/[S]ave [A]bort' )
# now loop
pos=0
message, lastmessage = None, None
while True:
# Paint all the fields
y, x = 2, 2
for i, field in enumerate( EDITFIELDS ):
editwin.addstr( y, x, field['name']+':', curses.A_BOLD )
editwin.addnstr( y, x+15, _getfield( world, field ) + " "*80, cols-19,
i==pos and curses.A_REVERSE or curses.A_NORMAL )
y += 1
# get input
while True:
# show the help for the current item
field = EDITFIELDS[pos]
editwin.addnstr( y+1, 2, field['help']+(" "*80), cols-4 )
# get input
editwin.move( 2+pos, 17 )
editwin.refresh()
c = editwin.getkey()
if c in ( 'i', 'KEY_UP', '\x10' ):
if pos>0:
pos -= 1
break
elif c in ( 'j', 'KEY_DOWN', '\x0e' ):
if (pos+1) < len( EDITFIELDS ):
pos += 1
break
elif c in ( 'e', 'KEY_ENTER', ' ', '\n', 'd', 'KEY_DC' ):
value = _getfield( world, field )
if c in ( 'd', 'KEY_DC' ):
value = ''
elif 'edit' in field:
value = field['edit'](world, value )
else:
# create a text editing box
linewin = editwin.derwin( 1, cols-19, 2+pos, 17 )
linewin.erase()
if c != ' ':
linewin.addstr( 0, 0, value )
textpad = curses.textpad.Textbox( linewin )
curses.noecho()
value = textpad.edit().strip()
if ' ' in value:
message = 'No spaces allowed in values'
elif 'validate' in field:
message = field['validate']( world, worldnames, value )
if not message:
_setfield( world, field, value )
else:
_setfield( world, field, value )
break
elif c in ( 'y', 'n', 'Y', 'N' ):
if 'yesno' in field:
_setfield( world, field, c )
break
elif c in ( 'S', 'Q' ):
# return world only if it has a name
if world.category!=world.INVALID:
return world
else:
return None
elif c in ( 'A', 'KEY_ESC' ):
return None
message, lastmessage = _show_message( editwin, message, lastmessage )
tf.out( "% config.py loaded: use '/worlds' to edit your worlds" )