#!/usr/bin/env python

# Thu Dec  9 20:24:33 PST 2004
# Tossing my "ssh-grid" prototype.  It's great for small numbers of hosts, but
# it's going to fall apart with more than a few 10's of hosts or 10's of
# commands, even with abbreviations.  Also, ssh-grid started out too
# procedurally - this is a large enough project that a more OOP approach is
# almost certainly going to prove highly beneficial

# future plans (from ssh-grid, but mostly applies here too) :
#
# 1) Need a "kill" option, EG For mozilla's and jpilot's that crash.
# That's just another command.  Already done effectively for evolution.
#
# 2) Need to be able to specify a long hostname and a short hostname,
# defaulting to the obvious.  Done.
#
# 3) Need to sort out why ssh X tunneling is unreliable with FC[23]!  Done.
#
# 4) Need to be able to specify a short name for a command, as well as
# the full command!  EG, some commands want to specify a PATH or
# something...  That shouldn't clutter up the GUI.  Done.
#
# 5) It'd be really sweet to have some sort of "recent pairings" pane!
#
# 6) Continue work on "multiple" option.  Done.
#
# 7) Add some explanatory tooltips.  Done.
#
# 8) Make buttons more persistent.  Done.
#
# 9) Add a "submit" button for multiple option.  Done.
#
# 10) Grey out some things?  There are obvious things, like greying out
# the submit button when multiple isn't in effect, but what about the
# possibility of checking if various commands are present on a system,
# and greying them (perhaps with an override) if they don't show up on a
# particular path?  That'd be really cool.
#
# 11) Look into why "mozilla -dont" isn't flying.  Is it a "two word"
# problem in deep-ssh, or is it just some sort of x11 forwarding thing I
# was seeing?  Done.
#
# 12) what about "worker" and "xfe" (or was it "xce"?)?  Are they worth
# copying around?  Is a generic "propagate this to target system" option
# worthwhile?  Probably best done as "just another command".
#
# 13) file transfers?  -Now- wer're starting to get ambitious.  :)
# Maybe sftpserv and/or xcb?  Or both :)  sftpserv is more likely to be
# present on your random system though.
#
# 14) Maybe make deep-ssh into a python module...  Or simply merge it...
#
# 15) Maybe have ssh-grid - like functionality if the user so chooses...
# There may be other alternative interfaces that'd be nice too.
# (ssh-grid was just a matrix of buttons, with the (EG) rows being
# labeled with hostnames, and the columns being labeled with commands.
# It was great for small numbers of each, but grew unwieldy much more
# quickly than sshgui's interface.)
#
# 16) Change stderr errors to a dialog.  Done.
#
# 17) Rewrite?  There's a -Lot- of potential for using inheritance in
# this application, which I really haven't capitalized on as yet...  In
# fact, while some of the code is heavily parameterized (but still no
# inheritance), other portions are just cut-and-paste-and-substitutes...
 
import pygtk
pygtk.require('2.0')
import gtk
import os
import string
import sys
import time
import copy


class hostgroup_class:

	def __init__(self,name,hosts):
		self.name = name
		self.hosts = hosts

def tempfn():
	return '/tmp/sshgui-%d-%d' % (os.getpid(), int(time.time() * 10000))

class commandgroup_class:

	def __init__(self,name,commands):
		self.name = name
		self.commands = commands

class hostpath_class:

	def __init__(self,path,shortname,group):
		# "path" may be a hostname, or a bang path of hostnames - used for actual
		# execution of commands
		self.path = path
		self.pathlist = string.splitfields(self.path,'!')
		# shortname is the representation of the path in the GUI
		# this should be done on the fly - not memorized
		if shortname == '':
			list = []
			for host in self.pathlist:
				f = string.splitfields(host,'.')
				if len(f) == 1:
					list.append(host)
				else:
					list.append(f[0])
			self.shortname = string.joinfields(list,'!')
		else:
			self.shortname = shortname
		# revpathlist is for doing hostpath comparisons - IE, sorting
		self.revpathlist = string.splitfields(self.shortname)
		self.revpathlist.reverse()
		for i in range(len(self.revpathlist)):
			f = string.splitfields(self.revpathlist[i],'@')
			if len(f) == 1:
				# nothing to change
				pass
			elif len(f) == 2:
				self.revpathlist[i] = f[1]
			else:
				self.warn('Strange number of @ signs in hostpath: %s\nUsing %s'
					% (self.revpathlist[i],f[1]))
				self.revpathlist[i] = f[1]

		# What kind of host is this?  Used for grouping
		self.group = group

	def __cmp__(self,other):
		l1 = len(self.revpathlist)
		l2 = len(other.revpathlist)
		m = min(l1,l2)
		for i in range(m):
			if self.revpathlist[i] < other.revpathlist[i]:
				#print self.revpathlist[i],'<',other.revpathlist[i]
				return -1
			elif self.revpathlist[i] > other.revpathlist[i]:
				#print self.revpathlist[i],'>',other.revpathlist[i]
				return 1
		# equal to this point
		if l1 < l2:
			# l1 is shorter, so it's less
			#print l1,'<',l2
			return -1
		elif l1 > l2:
			# l1 is longer, so it's more
			#print l1,'>',l2
			return 1
		else:
			# lists are identical, result is equal
			#print l1,'==',l2
			return 0

	def __repr__(self):
		return self.path

	def __str__(self):
		return self.shortname

	def write(self,file):
		file.write(self.path+'\0'+self.shortname+'\0'+self.group+'\0')

class command_class:

	def __init__(self,command,shortname,group,rununder):
		# "path" is the full command, along with with any $PATH specification
		self.command = command
		# shortname is the representation of the path in the GUI
		if shortname == '':
			# this should do some form of abbreviation - and it shouldn't
			# memorize it for all time, either
			self.shortname = self.command
		else:
			self.shortname = shortname
		# What kind of command is this?  Used for grouping
		self.group = group
		# What command do we run this under?
		self.rununder = rununder

	def __cmp__(self,other):
		if self.shortname < other.shortname:
			return -1
		elif self.shortname > other.shortname:
			return 1
		else:
			return 0

	def __repr__(self):
		return self.command

	def __str__(self):
		return self.shortname

	def write(self,file):
		file.write(self.command+'\0'+self.shortname+'\0'+self.group+'\0'+ \
			self.rununder+'\0')

# this eliminated a lot a redundancy, in a way I've never seen a language able
# to before.  :)  Maybe smalltalk could too though.
def read_it(fn,claz,fields_should_be):
	# needs locking!
	sys.stderr.write('read_it needs locking!\n')
	n = len(fields_should_be)
	filename=os.path.expanduser(fn)
	try:
		os.stat(filename)
	except:
		file = open(filename,'w')
		file.write(str(len(fields_should_be))+'\0')
		for field in fields_should_be:
			file.write(field+'\0')
		file.close()
	try:
		file = open(filename,'r')
	except:
		self.error('Could not open %s\n' % filename)
	try:
		content=file.read()
	except:
		self.error('Could not read %s\n' % filename)
	try:
		file.close()
	except:
		self.error('Could not close %s\n' % filename)
	fields = string.splitfields(content,'\0')
	try:
		fields_per_record = string.atoi(fields[0])
	except:
		self.error('First field of %s must be the ASCII number %s\n' % n)
	if fields_per_record != n:
		self.error('First field of %s must be the ASCII number %s\n' % n)
	del fields[0]
	for fieldno in range(fields_per_record):
		if fields[fieldno] != fields_should_be[fieldno]:
			self.error("Field %d of %s must be %s\n" % \
				(fieldno, filename, fields_should_be[fieldno]))
	# OK, we have the right number of fields in a record, and the field names
	# are correct
	#
	# Now see if we have the right number of records!
	len_of_fields = len(fields)
	records = len_of_fields / fields_per_record
	if records * fields_per_record + 1 != len_of_fields:
		self.error("Incorrect number of fields in %s\n" % filename)
	# some editors will want to put a newline at the end of the file.  Cope.
	if fields[-1] != '' and fields[-1] != '\n':
		self.error("Last record of %s should have a single empty field\n" % fn)
	del fields[-1]
	groups = {}
	# skip the first "record", because it's just a decription of the file
	# format - IE, that's why we start at 1, instead of 0
	for recordno in range(1,records):
		instance = apply(claz, \
			fields[recordno*fields_per_record:(recordno+1)*fields_per_record])
		if groups.has_key(instance.group):
			groups[instance.group].append(instance)
		else:
			groups[instance.group] = [instance]
	return groups

def write_it(fn,groups,group,fields):
	# needs locking!
	sys.stderr.write('write_it needs locking!\n')
	n = len(fields)
	filename=os.path.expanduser(fn)
	try:
		file = open(filename+'.tmp','w')
	except:
		self.error('Could not open %s\n' % filename)
	file.write(str(len(fields))+'\0')
	for field in fields:
		file.write(field+'\0')
	# this is so beautifully indirect!  :)
	for grp in groups.keys():
		#print 'grp is',grp
		#print 'groups is',groups
		#print 'field group is',getattr(groups[grp],group)
		for item in getattr(groups[grp],group):
			for field in fields:
				file.write(getattr(item,field)+'\0')
	file.close()
	os.rename(filename+'.tmp',filename)

class GUI:
	def delete_command(self,command_to_del):
		found = 0
		for commandgroup in self.command_groups.keys():
			for commandno in range(len(self.command_groups[commandgroup].commands)):
				command = self.command_groups[commandgroup].commands[commandno]
				if command_to_del == command:
					del self.command_groups[commandgroup].commands[commandno]
					found += 1
		return found

	# untested!  Thu Dec 30 22:02:40 PST 2004
	# 
	# beginning testing, Sat Jan  1 10:58:29 PST 2005
	# mostly working, but fails to remove the hostgroup if it goes empty.  Sat Jan  1 11:22:24 PST 2005
	#
	# Getting closer.  The hostgroup is deleted now, but in moving a host
	# from one group to another, there is an empty position in the
	# destination hostgroup. Sun Jan  2 11:25:02 PST 2005
	def delete_host(self,host_to_del):
		found = 0
		for hostgroup in self.host_groups.keys():
			del_list = []
			for hostno in range(len(self.host_groups[hostgroup].hosts)):
				host = self.host_groups[hostgroup].hosts[hostno]
				if host_to_del == host:
					del_list.append(hostno)
					#del self.host_groups[hostgroup].hosts[hostno]
					found += 1
			# delete the hosts in reverse order
			del_list.reverse()
			for to_del in del_list:
				del self.host_groups[hostgroup].hosts[to_del]
			if len(self.host_groups[hostgroup].hosts) == 0:
				# the hostgroup has 0 hosts in it now, delete the hostgroup
				# too
				del self.host_groups[hostgroup]
		return found

	def execute_callback(self, widget, data=None):
		# unconditionally :)  But there's an error-check in do_run
		self.do_run()

	def deselect_callback(self, widget, data=None):
		#print 'deselect_callback'
		self.deselect_all()

	def do_nothing(self, widget, data=None):
		pass

	def about_callback(self, widget, data=None):
		dia = gtk.Dialog('About sshgui', self.window.get_toplevel(), gtk.DIALOG_DESTROY_WITH_PARENT, ("Done", 123))
		dia.set_size_request(500, 300)
		for line in [ \
			'sshgui is by Dan Stromberg', \
			'You are looking at version '+str(self.version), \
			'http://stromberg.dnsalias.org/~strombrg/sshgui/', \
			'Please see the FAQ at http://stromberg.dnsalias.org/~strombrg/sshgui/FAQ.html', \
			'strombrg@gmail.com' ]:
			dia.vbox.add(gtk.Label(line))
		dia.show_all()
		result = dia.run()
		if result == 123:
			dia.destroy()
			return 0
		else:
			# hm, that's weird...
			dia.destroy()
			self.error('Weird return from dialog in about_callback')
		
	def error(self, message):
		dia = gtk.Dialog('sshgui error!', self.window.get_toplevel(), gtk.DIALOG_DESTROY_WITH_PARENT, ("Done", 123))
		dia.vbox.add(gtk.Label(message))
		dia.show_all()
		result = dia.run()
		if result == 123:
			dia.destroy()
			sys.exit(1)
		else:
			# hm, that's weird...
			dia.destroy()
			sys.stderr.write('Weird return from dialog in error()')
			sys.exit(1)

	def warn(self, message):
		dia = gtk.Dialog('sshgui warning', self.window.get_toplevel(), gtk.DIALOG_DESTROY_WITH_PARENT, ("Done", 123))
		dia.vbox.add(gtk.Label(message))
		dia.show_all()
		result = dia.run()
		if result == 123:
			dia.destroy()
			return 0
		else:
			# hm, that's weird...
			dia.destroy()
			sys.stderr.write('Weird return from dialog in warn()')
			sys.exit(1)

	def help_callback(self, widget, data=None):
		dia = gtk.Dialog('sshgui help', self.window.get_toplevel(), gtk.DIALOG_DESTROY_WITH_PARENT, ("Done", 123))
		dia.set_size_request(300, 300)
		for line in [ \
			'If you want to run one command on one host, then either click', \
			'the host followed by the command, or vice-versa.', \
			' ', \
			'If you want to run n>=2 commands on one host, then click all', \
			'the commands you want, followed by the single host you want.', \
			' ', \
			'If you want to run 1 command on m>=2 hosts, then click all', \
			'the hosts you want, followed by the single command you want.', \
			' ', \
			'If you want to run n>=2 commands on m>=2 hosts, then first',
			'click the "Deferred" button, then click your commands and',
			'hosts in any order.  Finally, click the "Execute" button.',
			' ', \
			'To toggle the selection of an entire host group or command',
			'group at a time, click the button for the group.',
			' ', \
			'The carets ("^"), at least for now, are used as a reconfigure', \
			'button.', \
			]:
			dia.vbox.add(gtk.Label(line))
		dia.show_all()
		result = dia.run()
		if result == 123:
			dia.destroy()
			return 0
		else:
			# hm, that's weird...
			dia.destroy()
			self.error('Weird return from dialog in help_callback')
		
	# returns true if we need to write something, false otherwise
	def misc_config(self, title, data, fields, del_button,del_fn=None):
		# assumes everything's a string!
		if del_button:
			tuple=("Cancel", 123, "Save", 789, "Delete", 135)
		else:
			tuple=("Cancel", 123, "Save", 789)
		dia = gtk.Dialog(title, self.window.get_toplevel(), gtk.DIALOG_DESTROY_WITH_PARENT, tuple)
		dia.vbox.pack_start(gtk.Label(title))
		lenfields = len(fields)
		table = gtk.Table(2,lenfields,False)
		entries = []
		for fieldno in range(lenfields):
			table.attach(gtk.Label(fields[fieldno]),0,1,fieldno,fieldno+1)
			entry = gtk.Entry(256)
			entry.set_text(getattr(data,fields[fieldno]))
			entries.append(entry)
			table.attach(entry,1,2,fieldno,fieldno+1)
		dia.vbox.pack_start(table)
		dia.show_all()
		result = dia.run()
		if result == 123:
			# just destroy the dialog
			dia.destroy()
			return 0
		elif result == 456:
			# note that this has been disabled...
			# save results in memory only, then destroy the dialog
			for fieldno in range(lenfields):
				setattr(data,entries[fieldno].get_text())
			dia.destroy()
			return 0
		elif result == 789:
			# save results in memory and on disk, then destroy the dialog
			for fieldno in range(lenfields):
				setattr(data,fields[fieldno],entries[fieldno].get_text())
			# save the changes!
			#sys.stderr.write('Warning: not saving changes yet!')
			dia.destroy()
			return 1
		elif result == 135:
			# save results in memory and on disk, then destroy the dialog
			for fieldno in range(lenfields):
				setattr(data,fields[fieldno],entries[fieldno].get_text())
			# save the changes!
			#sys.stderr.write('Warning: not saving changes yet!')
			dia.destroy()
			#print del_fn
			del_result = apply(del_fn,[data])
			if del_result != 1:
				self.warn('Deleted '+str(del_result)+' items')
			self.draw()
			return 1
		else:
			# hm, that's weird...
			dia.destroy()
			self.error('Weird return from dialog in misc_config')

	def write_hosts(self):
		write_it('~/.sshgui/hosts', self.host_groups, 'hosts', ['path', 'shortname', 'group'])

	def write_commands(self):
		write_it('~/.sshgui/commands', self.command_groups, 'commands', ['command', 'shortname', 'group', 'rununder'])

	# there is a Lot of similarity between add_command_callback and
	# add_host_callback...  Maybe split out the common parts into an
	# add_it?
	def add_command_callback(self, widget, data=None):
		#print 'add command'
		data = command_class('','','','')
		#print 'before'
		#self.dump_command_groups()
		if self.misc_config('Add command ', data, ['command', 'shortname', 'group', 'rununder'], 0):
			command_found = 0
			command_group_found = 0
			for commandgroup in self.command_groups.keys():
				if data.group == commandgroup:
					command_group_found = 1
					for commandno in range(len(self.command_groups[commandgroup].commands)):
						if self.command_groups[commandgroup].commands[commandno] == data:
							# both the command_group and the command already
							# exit - replace.
							# maybe ask the user if the want to replace this...
							command_found = 1
							self.command_groups[commandgroup].commands[commandno] = data
					# stop hunting through the command groups.
					# "commandgroup" variable should be the key for the right
					# command group to modify
					break
			if command_group_found and not command_found:
				# append the new command to the command group's command list
				self.command_groups[commandgroup].commands.append(data)
			if not command_group_found:
				# This command group does not exist yet.  create a new
				# command group and add it to the command_groups dictionary
				commandgroup_instance = commandgroup_class(data.group,data)
				self.command_groups[data.group] = commandgroup_instance
			#print 'after'
			#self.dump_command_groups()
			self.write_commands()
			self.draw()

	def add_replace_host(self,data):
		host_found = 0
		host_group_found = 0
		for hostgroup in self.host_groups.keys():
			if data.group == hostgroup:
				host_group_found = 1
				for hostno in range(len(self.host_groups[hostgroup].hosts)):
					if self.host_groups[hostgroup].hosts[hostno] == data:
						# both the host_group and the host already
						# exit - replace.
						# maybe ask the user if the want to replace this...
						host_found = 1
						self.host_groups[hostgroup].hosts[hostno] = data
				# stop hunting through the host groups.
				# "hostgroup" variable should be the key for the right
				# host group to modify
				break
		if host_group_found and not host_found:
			# append the new host to the host group's host list
			self.host_groups[hostgroup].hosts.append(data)
		if not host_group_found:
			# This host group does not exist yet.  create a new
			# host group and add it to the host_groups dictionary
			hostgroup_instance = hostgroup_class(data.group,[data])
			self.host_groups[data.group] = hostgroup_instance
		#print 'after'
		#self.dump_host_groups()
		self.write_hosts()
		#read_hosts()
		self.draw()

	def add_host_callback(self, widget, data=None):
		data = hostpath_class('','','')
		sys.stdout.flush()
		if self.misc_config('Add host ', data, ['path', 'shortname', 'group'], 0):
			self.dump_host_groups()
			self.add_replace_host(data)
#			host_found = 0
#			host_group_found = 0
#			for hostgroup in self.host_groups.keys():
#				if data.group == hostgroup:
#					host_group_found = 1
#					for hostno in range(len(self.host_groups[hostgroup].hosts)):
#						if self.host_groups[hostgroup].hosts[hostno] == data:
#							# both the host_group and the host already
#							# exit - replace.
#							# maybe ask the user if the want to replace this...
#							host_found = 1
#							self.host_groups[hostgroup].hosts[hostno] = data
#					# stop hunting through the host groups.
#					# "hostgroup" variable should be the key for the right
#					# host group to modify
#					break
#			if host_group_found and not host_found:
#				# append the new host to the host group's host list
#				self.host_groups[hostgroup].hosts.append(data)
#			if not host_group_found:
#				# This host group does not exist yet.  create a new
#				# host group and add it to the host_groups dictionary
#				hostgroup_instance = hostgroup_class(data.group,data)
#				self.host_groups[data.group] = hostgroup_instance
#			#print 'after'
#			#self.dump_host_groups()
#			self.write_hosts()
#			self.draw()

	def dump_host_groups(self):
		for hostgroup in self.host_groups.keys():
			print 'hostgroup',hostgroup
			for host in self.host_groups[hostgroup].hosts:
				print '\thost'+str(host)

	def dump_command_groups(self):
		for commandgroup in self.command_groups.keys():
			print 'commandgroup',commandgroup
			for command in self.command_groups[commandgroup].commands:
				print '\tcommand'+str(command)

	def command_reconfig_callback(self, widget, data=None):
		if self.misc_config('Modify command '+data.shortname, data, ['command', 'shortname', 'group', 'rununder'], 1, self.delete_command):
			self.write_commands()
		self.draw()
#		elif result == gtk.RESPONSE_CLOSE:
#			do_user_just_closed_the_window()
#		dia.destroy()

	def host_reconfig_callback(self, widget, data=None):
		prior_data = copy.deepcopy(data)
		if self.misc_config('Modify host '+data.shortname, data, ['path', 'shortname', 'group'], 1, self.delete_host):
			if prior_data.group != data.group:
				self.delete_host(prior_data)
				self.add_replace_host(data)
			self.write_hosts()
		self.draw()
#		elif result == gtk.RESPONSE_CLOSE:
#			do_user_just_closed_the_window()
#		dia.destroy()

	def host_callback(self, widget, data=None):
		if widget.get_active():
			self.active_hosts[data.__repr__()] = data
			if self.deferred_button.get_active():
				# wait for the user to click "execute" before running
				# anything
				pass
			else:
				# check if something should be run right away
				self.check_run()
		else:
			if self.active_hosts.has_key(data.__repr__()):
				del self.active_hosts[data.__repr__()]

	def command_callback(self, widget, data=None):
		if widget.get_active():
			self.active_commands[data.__repr__()] = data
			if self.deferred_button.get_active():
				# wait for the user to click "execute" before running
				# anything
				pass
			else:
				# check if something should be run right away
				self.check_run()
		else:
			if self.active_commands.has_key(data.__repr__()):
				del self.active_commands[data.__repr__()]

	def check_run(self):
		if len(self.active_hosts) >= 1 and len(self.active_commands) >= 1:
			self.do_run()

	def do_run(self):
		#self.active_hosts.sort()
		#self.active_commands.sort()
		ah = self.active_hosts.keys()
		if len(ah) < 1:
			self.warn('Sorry, you need to select at least one host')
			return 0
		ah.sort()
		ac = self.active_commands.keys()
		if len(ac) < 1:
			self.warn('Sorry, you need to select at least one command')
			return 0
		ac.sort()
		for repetition in range(self.multiple_spinner.get_value_as_int()):
			for host in ah:
				for command in ac:
					print 'Will execute command',command,'on host',host
					# -t: allocate a tty even if a command is given
					# -4: use ipv4 only.  Fedora Core 3's sshd sometimes only
					#     sets up X11 forwarding for ipv6 without this.  Note:
					#     this did -not- help!
					# -c blowfish: faster encryption, ala Bruce Schneier
					cmd_to_write = 'deep-ssh "-t -c blowfish" "%s" "%s"' % (self.active_hosts[host].path,self.active_commands[command].command)
					fn = tempfn()
					file = open(fn,'w')
					file.write('#!/bin/sh\n')
					#file.write('# The file should exist in the filesystem, and continue to be visible\n')
					#file.write('# to the bourne shell, even if it no longer has a name...\n')
					# but it doesn't sometimes :)
					file.write('%s\n' % cmd_to_write)
					file.write('(sleep 600 && rm $0) &\n')
					file.close()
					os.chmod(fn,0700)
					if self.active_commands[command].rununder == '':
						# this is not a GUI command, so we run it in some form of
						# user-specified local terminal emulator or similar
						cmd = fn
					else:
						cmd = self.active_commands[command].rununder+" "+fn
					cmd = cmd + ' &'
					# maybe use daemon?  Or just setsid?
					print 'cmd is',cmd
					pid = os.fork()
					if pid == 0:
						# this setsid seems to help pursuade ssh to ask for
						# passwords and such graphically, instead of via a tty.
						# For this application at least, that's a good thing.
						os.setsid()
						#for fd in range(10):
						#	os.close(fd)
						os.system(cmd)
						time.sleep(self.secs_between_spinner.get_value_as_int())
						sys.exit(0)
					else:
						retval = os.waitpid(pid,0)
						print 'retval is',retval
		if self.stayopen_button.get_active():
			# if the user wants sshgui to stick around, just clear the
			# previous buttons selected
			#print 'stayopen button is active'
			self.deselect_all()
			self.multiple_spinner.set_value(1.0)
			self.secs_between_spinner.set_value(5.0)
		else:
			# otherwise we exit
			#self.window.destroy()
			#gtk.main_quit()
			# give the command(s) we just started time to fire up...
			# the user has a chance to change their mind :)
			time.sleep(8)
			if self.stayopen_button.get_active():
				self.deselect_all()
				self.multiple_spinner.set_value(1.0)
				self.secs_between_spinner.set_value(5.0)
			else:
				# exit!
				sys.exit(0)

	def toggle_hostgroup(self, widget, hostgroup):
		#print type(hostgroup)
		#print hostgroup
		#print hostgroup.__repr__()
		for host in hostgroup.hosts:
			host.select_button.set_active(not host.select_button.get_active())

	def toggle_commandgroup(self, widget, commandgroup):
		#print type(commandgroup)
		#print commandgroup
		#print commandgroup.__repr__()
		for command in commandgroup.commands:
			command.select_button.set_active(not command.select_button.get_active())

	def deselect_all(self):
		for host in self.active_hosts.keys():
			self.active_hosts[host].select_button.set_active(False)
		for command in self.active_commands.keys():
			self.active_commands[command].select_button.set_active(False)
		# this is really optional here, but removing it was masking an error I
		# wanted to track down, so I put it back
		#self.draw()

	def deferred_callback(self, widget, data=None):
		#print 'deferred callback'
		#self.draw()
		if self.deferred_button.get_active():
			self.execute_button.set_sensitive(True)
		else:
			self.execute_button.set_sensitive(False)

	# This callback quits the program
	def delete_event(self, widget, event, data=None):
		# might put in a "Are you sure you want to exit?", but probably
		# not for this app :)
		gtk.main_quit()
		return False

#	def set_global_color(self,button):
#		label = button.get_child()
#		#button.modify_fg(gtk.STATE_NORMAL, gtk.gdk.color_parse("red"))
#		label.modify_fg(gtk.STATE_NORMAL, gtk.gdk.color_parse("darkred"))

	# this had to be modified to handle the spin button
	def set_global_color(self,button):
		if hasattr(button,'get_child'):
			button.get_child().modify_fg(gtk.STATE_NORMAL, \
				gtk.gdk.color_parse("darkred"))

	def set_host_color(self,button):
		label = button.get_child()
		label.modify_fg(gtk.STATE_NORMAL, gtk.gdk.color_parse("darkgreen"))

	def set_command_color(self,button):
		label = button.get_child()
		label.modify_fg(gtk.STATE_NORMAL, gtk.gdk.color_parse("darkblue"))

	def draw(self):
		# create the vbox's, which will hold the hbox of configuration
		# buttons, and a table.  Delete it if it already existed, so we can start over with a clean slate
		#print 'starting draw()'
		if hasattr(self,'Vbox'):
			for child in self.window.get_children():
				self.window.remove(child)
			for child in self.Vbox.get_children():
				self.Vbox.remove(child)
			for child in self.host_vbox.get_children():
				self.host_vbox.remove(child)
			for child in self.command_vbox.get_children():
				self.command_vbox.remove(child)
			for child in self.scrolled_host_window.get_children():
				self.scrolled_host_window.remove(child)
			for child in self.scrolled_command_window.get_children():
				self.scrolled_command_window.remove(child)
			for child in self.hbox.get_children():
				self.hbox.remove(child)
		self.Vbox = gtk.VBox()
		self.scrolled_host_window = gtk.ScrolledWindow()
		self.scrolled_host_window.set_border_width(10)
		#self.scrolled_host_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
		self.scrolled_host_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
		self.scrolled_command_window = gtk.ScrolledWindow()
		self.scrolled_command_window.set_border_width(10)
		self.scrolled_command_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
		self.host_vbox = gtk.VBox()
		self.command_vbox = gtk.VBox()
		self.scrolled_host_window.add_with_viewport(self.host_vbox)
		self.scrolled_command_window.add_with_viewport(self.command_vbox)
#		if not hasattr(self,'hbox'):
		# create the hbox, which will hold our configuration buttons
		self.hbox = gtk.HBox()
		#cmap = self.hbox.get_colormap()
		#color=cmap.alloc_color('red')
		#self.hbox.modify_fg(gtk.STATE_NORMAL, color)

		# the stay open button
		self.stayopen_button = gtk.ToggleButton('Stay open')
		self.set_global_color(self.stayopen_button)
		self.tooltips.set_tip(self.stayopen_button,"Keeps sshgui open after running a host x command pairing")
		self.stayopen_button.connect("clicked", self.do_nothing)
		self.stayopen_button.show()
		self.hbox.pack_start(self.stayopen_button)

		# the deferred button.  This really needs a tooltip
		if not hasattr(self,'deferred_button'):
			self.deferred_button = gtk.ToggleButton('Deferred')
			self.set_global_color(self.deferred_button)
			self.tooltips.set_tip(self.deferred_button,"Run n commands on m hosts")
			self.deferred_button.connect("clicked", self.deferred_callback)
			self.deferred_button.show()
		self.hbox.pack_start(self.deferred_button)

		# the execute button
		if not hasattr(self,'execute_button'):
			self.execute_button = gtk.Button('Execute')
			self.set_global_color(self.execute_button)
			self.tooltips.set_tip(self.execute_button,"Executes n commands on m hosts, after clicking the \"Deferred\" button")
			self.execute_button.connect("clicked", self.execute_callback)
			self.execute_button.show()
		if self.deferred_button.get_active():
			self.execute_button.set_sensitive(True)
		else:
			self.execute_button.set_sensitive(False)
		self.hbox.pack_start(self.execute_button)

		# the multiple spinner
		if not hasattr(self,'multiple_spinner'):
			#self.multiple_adj = gtk.Adjustment(1.0, 1.0, 99.0, 1.0, 2.0, 0.0)
			self.multiple_adj = gtk.Adjustment(1, 1, 9, 1, 1, 0)
			self.multiple_spinner = gtk.SpinButton(self.multiple_adj, 0, 0)
			self.multiple_spinner.set_digits(0)
			self.multiple_spinner.set_wrap(True)
			# this errors
			#self.multiple_spinner.set_shadow_type(gtk.SHADOW_OUT)
			self.hbox.pack_start(self.multiple_spinner,False)
			self.set_global_color(self.multiple_spinner)
			self.tooltips.set_tip(self.multiple_spinner,"Run the same host x command pairings multiple times")
			self.multiple_spinner.show()
		else:
			self.hbox.pack_start(self.multiple_spinner,False)

		# the secs_between spinner
		if not hasattr(self,'secs_between_spinner'):
			self.secs_between_adj = gtk.Adjustment(5, 0, 30, 1, 1, 0)
			self.secs_between_spinner = gtk.SpinButton(self.secs_between_adj, 0, 0)
			self.secs_between_spinner.set_digits(0)
			self.secs_between_spinner.set_wrap(True)
			# this errors
			#self.secs_between_spinner.set_shadow_type(gtk.SHADOW_OUT)
			self.hbox.pack_start(self.secs_between_spinner,False)
			self.set_global_color(self.secs_between_spinner)
			self.tooltips.set_tip(self.secs_between_spinner,"Seconds between command invocations")
			self.secs_between_spinner.show()
		else:
			self.hbox.pack_start(self.secs_between_spinner,False)

		# the deselect all button
		button = gtk.Button('Deselect all')
		self.set_global_color(button)
		self.tooltips.set_tip(button,"Deselects all hosts and commands")
		button.connect("clicked", self.deselect_callback)
		button.show()
		self.hbox.pack_start(button)

		# the add host button
		button = gtk.Button('Add host')
		self.set_global_color(button)
		button.connect("clicked", self.add_host_callback)
		button.show()
		self.hbox.pack_start(button)

		# the add command button
		button = gtk.Button('Add command')
		self.set_global_color(button)
		button.connect("clicked", self.add_command_callback)
		button.show()
		self.hbox.pack_start(button)

		# the Exit button
		button = gtk.Button('Exit')
		self.set_global_color(button)
		button.connect("clicked", lambda w: gtk.main_quit())
		button.show()
		self.hbox.pack_start(button)

		# the "About sshgui" button
		button = gtk.Button('About sshgui')
		self.set_global_color(button)
		button.connect("clicked", self.about_callback)
		button.show()
		self.hbox.pack_start(button)

		# the "Help" button
		button = gtk.Button('Help')
		self.set_global_color(button)
		button.connect("clicked", self.help_callback)
		button.show()
		self.hbox.pack_start(button)

		self.Vbox.pack_start(self.hbox,False,False)
		self.hbox.show()

		if 1 == 1:
			pass

		# Create a table which will hold a list of hosts
		hostgroups = self.host_groups.keys()
		hostgroups.sort()
		cols = 4
		for hostgroup in hostgroups:
			numhostgroups = len(hostgroups)
			# number of rows: round up
			rows = (numhostgroups + cols - 1) / cols

			if hasattr(self.host_groups[hostgroup],'HSeparator'):
				for widget in self.host_groups[hostgroup].table.get_children():
					#print widget
					self.host_groups[hostgroup].table.remove(widget)
				del self.host_groups[hostgroup].table
				self.host_vbox.pack_start(self.host_groups[hostgroup].HSeparator,False,False)
				self.host_groups[hostgroup].HSeparator.show()
				self.host_vbox.pack_start(self.host_groups[hostgroup].hostgroup_button,False,False)
				self.host_groups[hostgroup].hostgroup_button.show()
				self.host_groups[hostgroup].table = gtk.Table(cols, rows, False)
			else:
				self.host_groups[hostgroup].HSeparator = gtk.HSeparator()
				self.host_groups[hostgroup].HSeparator.show()
				#self.host_groups[hostgroup].HSeparator = gtk.HSeparator()
				#self.host_groups[hostgroup].HSeparator.show()
				self.host_vbox.pack_start(self.host_groups[hostgroup].HSeparator)

				self.host_groups[hostgroup].hostgroup_button = gtk.Button(hostgroup)
				self.set_host_color(self.host_groups[hostgroup].hostgroup_button)
				self.host_groups[hostgroup].hostgroup_button.connect('clicked',self.toggle_hostgroup,self.host_groups[hostgroup])
				self.tooltips.set_tip(self.host_groups[hostgroup].hostgroup_button,"Toggle selection of this host group")
				self.host_groups[hostgroup].hostgroup_button.show()
				self.host_vbox.pack_start(self.host_groups[hostgroup].hostgroup_button)
				self.host_groups[hostgroup].table = gtk.Table(cols, rows, False)

			hosts = self.host_groups[hostgroup].hosts
			#print 'before sort',hosts
			hosts.sort()
			#print 'after sort',hosts
			numhosts = len(hosts)
			for hostno in range(numhosts):
				row = hostno / cols
				col = hostno - row*cols
				# self.hostgroups is a dict of items of type hostgroup_class, indexed by hostgroup name
				#print self.host_groups
				if hasattr(self.host_groups[hostgroup].hosts[hostno],'select_button'):
					# reattach the already created button - possibly in a different place on the screen
					self.host_groups[hostgroup].table.attach(self.host_groups[hostgroup].hosts[hostno].hbox,col,col+1,row,row+1)
				else:
					self.host_groups[hostgroup].hosts[hostno].hbox = gtk.HBox()
					self.host_groups[hostgroup].hosts[hostno].config_button = gtk.Button('^')
					self.set_host_color(self.host_groups[hostgroup].hosts[hostno].config_button)
					#self.tooltips.set_tip(self.host_groups[hostgroup].hosts[hostno].config_button,
					#	"Modify " + self.host_groups[hostgroup].hosts[hostno].shortname)
					#self.tooltips.set_tip(self.host_groups[hostgroup].hosts[hostno].select_button,
					#	"Select " + self.host_groups[hostgroup].hosts[hostno].path)
					self.host_groups[hostgroup].hosts[hostno].select_button = \
						gtk.ToggleButton()
					self.host_groups[hostgroup].hosts[hostno].hbox.pack_start( \
						self.host_groups[hostgroup].hosts[hostno].config_button, False,True)
					self.host_groups[hostgroup].hosts[hostno].hbox.pack_start(self.host_groups[hostgroup].hosts[hostno].select_button, \
						False,True)
					self.host_groups[hostgroup].hosts[hostno].select_button.connect("clicked",self.host_callback, \
						self.host_groups[hostgroup].hosts[hostno])
					self.host_groups[hostgroup].hosts[hostno].config_button.connect("clicked",self.host_reconfig_callback, \
						self.host_groups[hostgroup].hosts[hostno])
					self.host_groups[hostgroup].hosts[hostno].select_button.show()
					self.host_groups[hostgroup].hosts[hostno].config_button.show()
					self.host_groups[hostgroup].hosts[hostno].hbox.show()
					self.host_groups[hostgroup].table.attach(self.host_groups[hostgroup].hosts[hostno].hbox, \
						col,col+1,row,row+1)
				# change the button's label whether the button preexisted or
				# not
				self.host_groups[hostgroup].hosts[hostno].select_button.set_label(self.host_groups[hostgroup].hosts[hostno].shortname)
				# change the label's color, whether the button preexisted or
				# not
				self.set_host_color(self.host_groups[hostgroup].hosts[hostno].select_button)
				# redefine the relevant tooltips regardless as well...
				self.tooltips.set_tip(self.host_groups[hostgroup].hosts[hostno].config_button,
					"Modify " + self.host_groups[hostgroup].hosts[hostno].shortname)
				self.tooltips.set_tip(self.host_groups[hostgroup].hosts[hostno].select_button,
					"Select " + self.host_groups[hostgroup].hosts[hostno].path)
			self.host_groups[hostgroup].table.show()
			self.host_vbox.pack_start(self.host_groups[hostgroup].table)

		self.host_vbox.show()
		self.Vbox.pack_start(self.scrolled_host_window)

		# Create a table which will hold a list of commands
		commandgroups = self.command_groups.keys()
		commandgroups.sort()
		#cols = 4
		for commandgroup in commandgroups:
			numcommandgroups = len(commandgroups)
			# number of rows: round up
			rows = (numcommandgroups + cols - 1) / cols

			if hasattr(self.command_groups[commandgroup],'HSeparator'):
				for widget in self.command_groups[commandgroup].table.get_children():
					self.command_groups[commandgroup].table.remove(widget)
				del self.command_groups[commandgroup].table
				self.command_vbox.pack_start(self.command_groups[commandgroup].HSeparator,False,False)
				self.command_groups[commandgroup].HSeparator.show()
				self.command_vbox.pack_start(self.command_groups[commandgroup].commandgroup_button,False,False)
				self.command_groups[commandgroup].commandgroup_button.show()
				self.command_groups[commandgroup].table = gtk.Table(cols, rows, False)
			else:
				self.command_groups[commandgroup].HSeparator = gtk.HSeparator()
				self.command_groups[commandgroup].HSeparator.show()
				self.command_vbox.pack_start(self.command_groups[commandgroup].HSeparator)

				self.command_groups[commandgroup].commandgroup_button = gtk.Button(commandgroup)
				self.set_command_color(self.command_groups[commandgroup].commandgroup_button)
				self.command_groups[commandgroup].commandgroup_button.connect('clicked',self.toggle_commandgroup,self.command_groups[commandgroup])
				self.tooltips.set_tip(self.command_groups[commandgroup].commandgroup_button,"Toggle selection of this command group")
				self.command_groups[commandgroup].commandgroup_button.show()
				self.command_vbox.pack_start(self.command_groups[commandgroup].commandgroup_button)
				self.command_groups[commandgroup].table = gtk.Table(cols, rows, False)

			commands = self.command_groups[commandgroup].commands
			commands.sort()
			numcommands = len(commands)
			for commandno in range(numcommands):
				row = commandno / cols
				col = commandno - row*cols
				# self.commandgroups is a dict of items of type commandgroup_class, indexed by commandgroup name
				#print self.command_groups
				if hasattr(self.command_groups[commandgroup].commands[commandno],'select_button'):
					# reattach the already created button - possibly in a different place on the screen
					self.command_groups[commandgroup].table.attach(self.command_groups[commandgroup].commands[commandno].hbox,col,col+1,row,row+1)
				else:
					self.command_groups[commandgroup].commands[commandno].hbox = gtk.HBox()
					self.command_groups[commandgroup].commands[commandno].config_button = gtk.Button('^')
					self.set_command_color(self.command_groups[commandgroup].commands[commandno].config_button)
					self.tooltips.set_tip(self.command_groups[commandgroup].commands[commandno].config_button, \
						"Modify " + self.command_groups[commandgroup].commands[commandno].shortname)
					self.command_groups[commandgroup].commands[commandno].select_button = \
						gtk.ToggleButton(self.command_groups[commandgroup].commands[commandno].shortname)
					self.set_command_color(self.command_groups[commandgroup].commands[commandno].select_button)
					self.tooltips.set_tip(self.command_groups[commandgroup].commands[commandno].select_button, \
						"Select " + self.command_groups[commandgroup].commands[commandno].command)
					self.command_groups[commandgroup].commands[commandno].hbox.pack_start( \
						self.command_groups[commandgroup].commands[commandno].config_button, False,True)
					self.command_groups[commandgroup].commands[commandno].hbox.pack_start(self.command_groups[commandgroup].commands[commandno].select_button, \
						False,True)
					self.command_groups[commandgroup].commands[commandno].select_button.connect("clicked",self.command_callback, \
						self.command_groups[commandgroup].commands[commandno])
					self.command_groups[commandgroup].commands[commandno].config_button.connect("clicked",self.command_reconfig_callback, \
						self.command_groups[commandgroup].commands[commandno])
					self.command_groups[commandgroup].commands[commandno].select_button.show()
					self.command_groups[commandgroup].commands[commandno].config_button.show()
					self.command_groups[commandgroup].commands[commandno].hbox.show()
					self.command_groups[commandgroup].table.attach(self.command_groups[commandgroup].commands[commandno].hbox, \
						col,col+1,row,row+1)
			self.command_groups[commandgroup].table.show()
			self.command_vbox.pack_start(self.command_groups[commandgroup].table)

		self.command_vbox.show()
		self.Vbox.pack_start(self.scrolled_command_window)

		self.scrolled_host_window.show()
		self.scrolled_command_window.show()
		self.Vbox.show()
		self.window.add(self.Vbox)
		self.window.show()
		#print 'end of draw'

	def __init__(self):
		# Create a new window
		self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)

		# Set the window title
		self.window.set_title("sshgui")

		# Set a handler for delete_event that immediately
		# exits GTK.
		self.window.connect("delete_event", self.delete_event)

		# Sets the border width of the window.
		self.window.set_border_width(20)

		self.command_groups = read_commands()
		self.host_groups = read_hosts()

		self.window.set_size_request(1280, 950)

		self.active_hosts = {}
		self.active_commands = {}

		self.tooltips = gtk.Tooltips()

		self.version = 0.5

		self.draw()

def read_hosts():
	dict = read_it('~/.sshgui/hosts', hostpath_class, [ 'path', 'shortname', 'group' ])
	keys = dict.keys()
	host_groups = {}
	for key in keys:
		#print key,dict[key]
		host_groups[key] = hostgroup_class(key,dict[key])
	return host_groups

def read_commands():
	dict = read_it('~/.sshgui/commands', command_class, [ 'command', 'shortname', 'group', 'rununder' ])
	keys = dict.keys()
	command_groups = {}
	for key in keys:
		command_groups[key] = commandgroup_class(key,dict[key])
	return command_groups

def main():
    gtk.main()
    return 0       

if __name__ == "__main__":
    GUI()
    main()