#!/usr/bin/env python

# This is the "boil the ocean" version :)

# before this, there was sshgui and sshgrid

# Filesystem structure:
# 	~/.hcm
#			/hosts
#						/host_groupname1
#											/hostname1
#											/hostname2
#											...
#											/hostnamen
#						/host_groupname2
#											/hostname1
#											/hostname2
#											...
#											/hostnamen
#						...
#						/host_groupnamen
#											/hostname1
#											/hostname2
#											...
#											/hostnamen
#			/commands
#						/command_groupname1
#											/commandname1
#											/commandname2
#											...
#											/commandnamen
#						/command_groupname2
#											/commandname1
#											/commandname2
#											...
#											/commandnamen
#						...
#						/command_groupnamen
#											/commandname1
#											/commandname2
#											...
#											/commandnamen
#
# So for example, the second node in Teoco would be:
#	~/.hcm/hosts/Teoco/teoco!node2
#
# As another example, the xterm command in the X11 command group would look like:
#	~/.hcm/commands/X11/xterm
#
# So inhaling all the command groups amounts to cd'ing to the ~/.hcm/commands directory, and inhaling
# all the directories as command groups
#
# Then inhaling a command group amounts to cd'ing to ~/.hcm/commands/X11 (for example), and inhaling
# all the files as commands
#
# So our group_of_groups_class needs ~/.hcm/commands in an instance variable
#
# Our command_group instance (which is a genericgroup_class instance, really) needs ~/.hcm/commands/command_groupname
# in an instance variable
#
# Our command_class needs the file ~/.hcm/commands/command_groupname/commandname in an instance variable

#import traceback


# There are two kinds of deletions in the program:
# 1) In which we delete a single item.  This means:
#		A) Delete the item itself and remove it from the GUI
#		B) Delete the item from the containing genericgroup_class instance (IE element of host_group or element of command_group).  Nothing needs
#			to change in the GUI for this part
# 2) In which we delete a entire hostgroup.  This means:
#		A) Delete all items in the hostgroup or command group, and remove them from the GUI
#     B) Delete the hostgroup or commandgroup instance itself (IE element of host_group or command_group) and remove its name from the GUI too
#		C) Delete the remnant of the hostgroup or commandgroup from the containing group_of_groups
#
# In both cases, it's probably best to initiate the relevant deletions from the most general container, descend down to the most specific,
# and delete backward in generality as we traverse back
# 
# The question arises, is there much commonality between the two?  Can #2 wrap #1 plus a bit?
#
# first of all:
# group_of_groups_class instances contain multiple genericgroup_class instances
# genericgroup_class instances contain multiple item_class instances
#
# #1 looks like:
# In genericgroup_class instance:
#	delete single item_class instance
#	delete item from genericgroup_class instance
#	rebuild genericgroup_class portion of GUI - or the whole thing?
#
# #2 looks like:
# In group_of_groups instance:
#	descend into single relevant genericgroup_class instance
#	for each item in genericgroup_class instance:
#		delete single item
#		delete item from genericgroup_class instance
#		don't rebuild the GUI just yet - it'll change more times than really necessary if one does
#	return back to the group_of_groups_instance
#	delete the genericgroup_class instance 
# 	rebuild the relevant part of the GUI - or the whole thing?

# Wow, this whole frame for a function thing...
# there should be frames for:
# 1) Selecting things to run and running them
#    A) Just a list of hostgroups, hostnames, command groups and command names.  To keep things simple, we probably only
#       need a toggle function, unless perhaps we get a little fancier with the host groups and command groups
#    B) This is the point of this program, so is the highest priority :)
#    C) Inherits from generic_operation_gui_builder
# 2) Modifying an item's details (excluding containing groups)
#    A) Just a list of hostnames and command names; no groups needed.  Click an expander, get a bunch of questions
#    B) This is relatively difficult to do with a text editor, mostly becaus of the lengths that need to be preserved.
#       However, it would not be terribly difficult to come up with a format that Could be modified with a text editor...   Hmm...
#    C) Inherits from generic_operation_gui_builder
# 3) Adding an item to a genericgroup (EG, a host to a preexisting hostgroup)
#    B) Adding a host to a hostgroup
#    D) Adding a command to a command group
#    E) This is relatively difficult to do with standard *ix shell commands, so is a relatively high priority.  However, it's not a really
#       high priority, because this program's commandline interface allows this operation
#    F) Does -not- inherit from generic_operation_gui_builder
# 4) Deleting items from a genericgroup
#    A) Just a list of hostgroups, hostnames, command groups and command names
#    B) Inherits from generic_operation_gui_builder
#    C) Nonessential - this is easy to do with rm
#    D) Inherits from generic_operation_gui_builder
# 5) Renaming items
#    A) for each of hosts and commands
#       I) A list of items
#       II) A "rename selected button"
#    B) Nonessential - this is easy to do with mv
#    C) Inherits from generic_operation_gui_builder
# 6) Merging host groups
#    A) Just a list of host groups and command groups, but each group listed is a menu of other groups one might merge with
#    B) Nonessential - this is easy to do with mv
#    C) Inherits from generic_operation_gui_builder
# 7) Splitting host groups or command groups
#    A) A list of items within groups, with the items togglable and the groups toggling all items within them
#    B) A text entry box for the new hostgroup name
#    C) Nonessential - this is easy to do with mkdir and mv
#    D) Inherits from generic_operation_gui_builder
# 8) Adding a hostgroup or commandgroup (genericgroup)
#    A) Adding a hostgroup
#    B) Adding a commandgroup
#    C) Nonessential - this is easy to do with mkdir
#    D) Does -not- inherit from generic_operation_gui_builder
# 9) Deleting genericgroups
#    A) Just a list of hostgroups and command groups
#    B) Does -not- inherit from generic_operation_gui_builder
#    C) Nonessential - this is easy to do with rm -rf and/or rm in combination with rmdir
#    D) Does -not- inherit from generic_operation_gui_builder
# 
# In each of these scenarios, we can employ a Frame under a Notebook.  I've got it doing a notebook now, but in retrospect, maybe a
# bunch of expanders would've been nice...
#
# In each of these scenarios that involve a list of items within hostgroups and commandgroups, we would benefit from a class
# that knows:
# 1) how to put some operation-specific stuff across the top of the frame
# 2) how to iterate over all items, and how to set up a button for each item - with the former being in a base clase, and the latter
#    being in distinct subclasses for each task

import os
import re
import sys
import time
import fcntl
import string
import shutil

sys.path.append('/usr/local/lib')
sys.path.append(os.path.expanduser('~/lib'))
import bashquote

program_name = 'hcm'
program_version=0.4

def gui_up():
	if 'gtk' in sys.modules.keys():
		return True
	else:
		return False

def ensure_directory(directory):
	try:
		os.mkdir(directory)
	except (OSError, 17):
		pass

def my_chdir(directory):
	ensure_directory(directory)
	os.chdir(directory)

def ignore(filename):
	if filename.startswith('.'):
		# skip dot files
		return 1
	if filename.endswith('~'):
		# skip emacs backup files
		return 1
	return 0

base_directory=os.path.join(os.path.expanduser('~'), '.%s' % program_name)
ensure_directory(base_directory)

# This callback quits the program
def delete_event(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 save_changes_and_exit(*args):
	print 'Saving changes...'
	host_group.exhale()
	command_group.exhale()
	print 'Terminating normally...'
	if gui_up():
		return delete_event('ignored1', 'ignored2')
	else:
		sys.exit(0)


def strip_eol(line):
	if line.endswith('\n'):
		return line[:-1]
	else:
		return line


def list_operation(left_list, right_list, function_for_left_list_only, function_for_right_list_only, function_for_in_both_lists):
	temp_left_list = left_list[:]
	temp_right_list = right_list[:]
	temp_left_list.sort()
	temp_right_list.sort()
	left_list_index = 0
	right_list_index = 0
	left_list_length = len(temp_left_list)
	right_list_length = len(temp_right_list)
	while left_list_index < left_list_length or right_list_index < right_list_length:
		if left_list_index >= left_list_length:
			# left_list is used up - add something from right_list
			function_for_right_list_only(temp_right_list[right_list_index])
			right_list_index += 1
		elif right_list_index >= right_list_length:
			# right_list is used up - add something from left_list
			function_for_left_list_only(temp_left_list[left_list_index])
			left_list_index += 1
		# neither list is used up
		elif temp_left_list[left_list_index] < temp_right_list[right_list_index]:
			# left_list's value is lower - add it
			function_for_left_list_only(temp_left_list[left_list_index])
			left_list_index += 1
		elif temp_left_list[left_list_index] > temp_right_list[right_list_index]:
			# right_list's value is lower - add it
			function_for_right_list_only(temp_right_list[right_list_index])
			right_list_index += 1
		else:
			# left_list's value is the same as right_list's, add them together
			function_for_in_both_lists(temp_left_list[left_list_index])
			left_list_index += 1
			right_list_index += 1


def temp_filename():
	return '/tmp/%s-%d-%d' % (program_name, os.getpid(), int(time.time() * 10000))

def hex_nybble(nybble):
	return '0123456789abcdef'[nybble]

def hex_byte(byte):
	return '%s%s' % (hex_nybble(byte/16), hex_nybble(byte & 0xf))

def random_string():
	file = open('/dev/urandom', 'r')
	bytes = file.read(16)
	file.close()
	result = ''
	for byte in bytes:
		result += hex_byte(ord(byte))
	return result

def logging_stamp():
	if 1 == 1:
		return '`logging-stamp`'
	else:
		# this is a nice thought, but then it's the same file for all tabs in
		# mrxvt, which isn't so nice ^_^
		return '%s-R%s' % (time.strftime('%Y-%m-%d-%a'), random_string())

def run_individual_match(host, command):
	if host.dict['path'].startswith('!'):
		# Here we're making a deep-ssh bang path have a new interpretation - if the bang path starts with a bang,
		# then run it locally, not in deep-ssh.
		print 'Locally spawning command %s on host %s' % (command.dict['name'], host.dict['name'])
		path = host.dict['path'][1:]
		print 'path is', path
		bq = bashquote.bashquote()
		bq.add(path)
		command_to_system = '%s %s' % (command.dict['command'], str(bq))
	else:
		print 'Spawning command %s on host %s' % (command.dict['name'], host.dict['name'])
		# arcfour is apparently a little faster than blowfish, at least according to PSC's web page about ssh performance patches.
		# Null encryption is probably faster than that...  But that only would encrypt authentication, not data transmission.
		bq = bashquote.bashquote()
		bq.add(command.dict['command'])
		command_to_write = "deep-ssh  '%s' %s --ssh-options '-t -c arcfour'" % (host.dict['path'], str(bq))
		bq2 = bashquote.bashquote()
		bq2.add(command_to_write)
		logdir = os.path.expanduser('~/.hcm/logs')
		ensure_directory(logdir)
		os.chdir(logdir)
		command_to_write = "pypty -d -f -c %s ~/.hcm/logs/%s" % (str(bq2), logging_stamp())
		filename = temp_filename()
		file = open(filename,'w')
		file.write('#!/bin/sh\n\n')
		file.write('if [ "$1" = "" ]\n')
		file.write('then')
		file.write('\ttitle="."\n')
		file.write('else')
		file.write('\ttitle="$1"\n')
		file.write('fi\n')
		file.write('which xname > /dev/null 2>&1 && xname "$title"\n\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')
		# except that it doesn't sometimes ^_^
		file.write('%s\n' % command_to_write)
		file.write('\n')
		# We don't actually want this rm, because a multi-tabbed terminal emulator that can run more than one invocation of a single command at
		# different times will get confused later when trying to spawn a new command from this file.  It'd be better to have a cron job
		# that cleans out anything over a couple of months old or something - but then sometimes my sessions last more than a couple of months...
		#file.write('(sleep 600 && rm $0) &\n')
		file.close()
		os.chmod(filename,0700)
		if command.dict['rununder'] == '':
			# this is a GUI command, so we do not need to run it in some form of user-specified local terminal
			# emulator or similar
			command_to_system = filename
		else:
			#print 'host.group_name is',host.group_name,'in run_individual_match'
			rununder_with_replacements = command.dict['rununder']. \
				replace('#g', host.group_name). \
				replace('#n', host.dict['name']). \
				replace('##', '#')
			#command_to_system = '%s %s "%s / %s"' % \
			#	(rununder_with_replacements, filename, host.group_name, host.dict['name'])
			command_to_system = '%s %s' % \
				(rununder_with_replacements, filename)
		command_to_system += ' &'
	# maybe use daemon?  Or just setsid?  I heard once that fork()'ing twice migh help, too
	print 'command_to_system is',command_to_system
	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()
		os.system(command_to_system)
		time.sleep(gui.builders['Run'].seconds_between_commands_spinner.get_value_as_int())
		sys.exit(0)
	else:
		retval = os.waitpid(pid,0)
		print 'retval is',retval

def run_all_host_command_matches():
	selected_hosts = host_group.get_selected('Run')
	selected_hosts.sort()
	selected_commands = command_group.get_selected('Run')
	selected_commands.sort()
	for repetition in range(gui.builders['Run'].command_repetition_spinner.get_value_as_int()):
		for host in selected_hosts:
			for command in selected_commands:
				run_individual_match(host, command)
	if gui.builders['Run'].stayopen_button.get_active():
		# if the stayopen button is active, then we don't automatically exit
		return
	else:
		# the stayopen button wasn't active, so we do exit. We may need to sleep a bit actually, but for now, we just exit
		sys.exit(0)

def run_if_appropriate_conditions():
	host_count = host_group.count_selected('Run')
	command_count = command_group.count_selected('Run')
	if gui.builders['Run'].deferred_button.get_active():
		# if we're in deferred mode, then we never run until the user clicks the execute button
		return
	else:
		# if we're not in deferred mode, then we run as soon as both the selected host count and the selected command count are above 0
		if host_count > 0 and command_count > 0:
			run_all_host_command_matches()

#gss_count=0
#sss=0

#toggle_selected_reentry_count = 0
set_selection_state_reentry_count = 0
toggle_selected_reentry_count = 0


class my_button:
	# this may help with inverting buttons...  Since get_active/set_active seem very confused
	def __init__(self, button, state):
		self.button = button
		self.state = state
		self.count = 0

	# this too_many/up/down stuff is basically a sloppy mutex
	def __too_many(self):
		if self.count >= 1:
			self.count = 0
			return 1
		else:
			return 0

	def __up(self):
		if self.count >= 0:
			self.count += 1
		else:
			self.count = 1

	def __down(self):
		if self.count >= 1:
			self.count -= 1
		else:
			self.count = 0

	def set_true(self):
		if self.__too_many():
			return
		self.__up()
		self.button.set_active(True)
		self.__down()
		self.state = True

	def set_false(self):
		if self.__too_many():
			return
		self.__up()
		self.button.set_active(False)
		self.__down()
		self.state = False

	def invert(self):
		if self.__too_many():
			return
		self.__up()
		self.state = not self.state
		self.button.set_active(self.state)
		self.__down()

	def get(self):
		return self.state

	def connect(self, *args):
		apply(self.button.connect, args)

	def get_children(self):
		if hasattr(self.button, 'get_children'):
			return self.button.get_children()
		else:
			return []
	
	def show(self):
		self.button.show()


# instances of this class will hold an individual host or an individual command
class item_class:
	def __init__(self, filename, group_name, *args):
		#print 'group_name is',group_name,'in item_class constructor'
		self.filename = filename
		self.group_name = group_name
		length = len(args)
		if length != 0 and length != len(self.my_fields):
			raise ("Bad number of arguments to item_class constructor: Wanted 2 or %d, got %d" % (len(self.my_fields)+2, len(args)+2))
		elif length == 0:
			if type(self.filename) == type(''):
				# assume this is a filename
				self.inhale(self.filename)
			else:
				raise ("Bad number of arguments to item_class constructor: Wanted 2 or %d, got %d" % (len(self.my_fields)+2, len(args)+2))
		elif length == len(self.my_fields):
			self.dict={}
			for fieldno in xrange(len(args)):
				self.dict[self.my_fields[fieldno]] = args[fieldno]
		else:
			sys.stderr.write(sys.argv[0], 'Eh?  This should not happen\n' % sys.argv[0])
			sys.exit(1)
		self.selected=False

	def noop(self, widget=None):
		pass
		return true

	def get_selection_state(self, button_type):
		return self.buttons[button_type].get()

	def mark_selected(self, button_type, individual):
		self.buttons[button_type].set_true()
		run_if_appropriate_conditions()

	def toggle_selected(self, button_type, widget=None, individual=True):
		self.buttons[button_type].invert()
		run_if_appropriate_conditions()

	def mark_deselected(self, button_type, individual):
		self.buttons[button_type].set_false()
		run_if_appropriate_conditions()

	def __cmp__(self, other):
		# only the name matters in comparisons
		if self.dict['name'] < other.dict['name']:
			return -1
		elif self.dict['name'] > other.dict['name']:
			return 1
		else:
			return 0

	def inhale(self, filename):
		self.filename = filename
		file = open(filename, 'r')
		number_of_fields = string.atoi(file.readline())
		fields_from_file = []
		for fieldno in xrange(number_of_fields):
			fields_from_file.append(strip_eol(file.readline()))

		if self.my_fields != fields_from_file:
			error('Field list mismatch in %s: wanted %s, got %s\n' % \
				(self.filename, str(fields_from_file), str(self.my_fields)))

		self.dict = {}
		for field in self.my_fields:
			try:
				length = string.atoi(file.readline())
			except ValueError:
				sys.stderr.write('%s: Not an integer in %s\n' % \
					(sys.argv[0], self.filename))
				sys.exit(1)

			self.dict[field] = strip_eol(file.read(length))
			dummy = file.readline()
			if dummy != '\n':
				sys.stderr.write('field anchor incorrect in %s\n' % self.filename)
		file.close()

	def exhale(self):
		file = open(self.filename, 'w')
		# write the number of fields, followed by a newline
		file.write(str(len(self.my_fields)))
		file.write('\n')
		# write the field names, separated by newlines
		for field in self.my_fields:
			file.write(field)
			file.write('\n')
		# write each field
		for field in self.my_fields:
			# write the field value length followed by a newline
			length = len(self.dict[field])
			file.write(str(length))
			file.write('\n')
			# write the field value followed by a newline
			file.write(self.dict[field])
			file.write('\n')
		file.close()

	def __str__(self):
		result = ''
		for field in self.my_fields:
			result += ' ' + self.dict[field]
		return result

	def setup_gui_objects(self):
		self.buttons = {}
		for button_type in ['Run']:
			self.buttons[button_type] = my_button(gtk.ToggleButton(self.dict['name']), self.selected)
			color_traversal(self.buttons[button_type], self.color)
			self.buttons[button_type].connect('toggled', lambda widget: self.toggle_selected(button_type, widget))

	# a little strangely named relative to this class in isolation since it will always be 0 or 1, but it makes sense
	# when considered in the context of some of the other classes in this program since they can have greater counts
	def count_selected(self, button_type):
		if self.get_selection_state(button_type):
			return 1
		else:
			return 0

	def delete(self):
		pass
		


# an instance of this class will hold an individual host
class host_class(item_class):
	my_fields = ['name', 'path', 'sshport']
	color = 'darkred'


# an instance of this class will hold an individual command
class command_class(item_class):
	my_fields = ['name', 'command', 'rununder']
	color = 'darkgreen'



# this will hold individual command groups and individual host groups - IOW, a group of hosts, or a group of commands
class genericgroup_class:
	def __init__(self, name, item_class_parameter, base_directory):
		self.name = name
		self.collection = {}
		#self.text_color = text_color
		self.base_directory = base_directory
		self.directory = os.path.join(self.base_directory, self.name)
		self.item_class = item_class_parameter # will be either host_class or command_class

	def mark_selected(self, button_type, data=None):
		keys = self.collection.keys()
		for key in keys:
			self.collection[key].mark_selected(button_type, False)
		run_if_appropriate_conditions()

	def toggle_selected(self, button_type, data=None):
		keys = self.collection.keys()
		for key in keys:
			self.collection[key].toggle_selected(button_type, False)
		run_if_appropriate_conditions()

	def mark_deselected(self, button_type, data=None):
		keys = self.collection.keys()
		for key in keys:
			self.collection[key].mark_deselected(button_type, False)
		# this should never actually be true anyway
		#run_if_appropriate_conditions()

	def add_or_replace(self, name, item):
		self.collection[name] = item

	def delete(self, item_name):
		if self.collection.has_key(item_name):
			del self.collection[item_name]
		else:
			sys.stderr.write('%s: %s %s already does not exist in %s group %s\n' % \
				(sys.argv[0], self.group_type, item_name, self.group_type, self.name))
			if not ignore_already:
				sys.exit(1)

	def keys(self):
		return self.collection.keys()

	def __getitem__(self, key):
		return self.collection[key]

	def __delitem__(self, key):
		del self.collection[key]

	def __str__(self):
		result = '%s' % self.name
		keys = self.collection.keys()
		keys.sort()
		# we're not including self.name in our output, because it's also in the item
		for key in keys:
			result += ' %s' % str(self.collection[key])
		return result

	def inhale(self):
		my_chdir(self.directory)
		self.collection = {}
		for itemname in os.listdir('.'):
			if ignore(itemname):
				continue
			self.collection[itemname] = self.item_class(os.path.join(self.directory, itemname), self.name)

	def exhale(self):
		in_memory_list = self.collection.keys()
		my_chdir(self.directory)
		on_disk_list = filter(lambda filename: not ignore(filename), os.listdir('.'))
		list_operation(in_memory_list, on_disk_list, \
			lambda itemname: self.collection[itemname].exhale(), \
			os.unlink, \
			lambda itemname: self.collection[itemname].exhale(), \
			)

	def hasitem(self, key):
		return key in self.collection

	def rename(self, widget=None):
		pass

	def delete(self, item_name):
		self.collection[item_name].delete()

	def count_selected(self, button_type):
		keys = self.collection.keys()
		selected = 0
		for key in keys:
			selected += self.collection[key].count_selected(button_type)
		return selected

	def get_selected(self, button_type):
		keys = self.collection.keys()
		selected = []
		for key in keys:
			if self.collection[key].get_selection_state(button_type):
				selected.append(self.collection[key])
		return selected


class host_group_class(genericgroup_class):
	# all host_groups will be dark red in the GUI
	color = 'darkred'
	group_type='host'
	#group_of_groups=host_group


class command_group_class(genericgroup_class):
	# all command_groups will be dark green in the GUI
	color = 'darkgreen'
	group_type='command'
	#group_of_groups=command_group


# there will be two of these - one for all host_groups, and one for all command_groups
class group_of_groups_class:
	def __init__(self, name, group_class_parameter, item_class_parameter, directory):
		self.name = name
		self.collection = {}
		self.group_class = group_class_parameter
		self.item_class = item_class_parameter
		self.directory = os.path.join(os.path.expanduser(directory), self.name)

	def keys(self):
		return self.collection.keys()

	def has_key(self, key):
		return self.collection.has_key(key)

	def __getitem__(self, key):
		return self.collection[key]

	def __setitem__(self, key, value):
		self.collection[key] = value

	# this one just reaks of abstraction violation, but I think that's probably essential
	def __delitem__(self, key):
		if self.collection.has_key(key):
			group = self.collection[key]
			for item in group.keys():
				group[item].delete()
				del group[item]
			del self.collection[key]
		else:
			sys.stderr.write('%s: %s group %s already did not exist\n' % (sys.argv[0], self.name, key))
			if not ignore_already:
				sys.exit(1)

	def inhale(self):
		my_chdir(self.directory)
		for filename in os.listdir('.'):
			if ignore(filename):
				continue
			self.collection[filename] = self.group_class(filename, self.item_class, self.directory)
			self.collection[filename].inhale()

	def exhale(self):
		in_memory_list = self.collection.keys()
		my_chdir(self.directory)
		on_disk_list = filter(lambda filename: not ignore(filename), os.listdir('.'))
		list_operation(in_memory_list, on_disk_list, \
			lambda groupname: self.collection[groupname].exhale(), \
			lambda directory_name: shutil.rmtree(os.path.join(self.directory, directory_name)), \
			lambda groupname: self.collection[groupname].exhale(), \
			)

	def for_gui(self):
		widget = gtk.VBox()
		temp = gtk.Label('groups of %s' % self.name)
		temp.show()
		widget.add(temp)
		keys = self.collection.keys()
		keys.sort()
		for key in keys:
			temp = self.collection[key].for_gui()
			temp.show()
			widget.pack_start(temp, expand=False, fill=False)
		return widget

	def mark_deselected(self, button_type):
		keys = self.collection.keys()
		for key in keys:
			self.collection[key].mark_deselected(False)

	def count_selected(self, button_type):
		keys = self.collection.keys()
		selected = 0
		for key in keys:
			selected += self.collection[key].count_selected(button_type)
		return selected

	def get_selected(self, button_type):
		keys = self.collection.keys()
		selected = []
		for key in keys:
			selected += self.collection[key].get_selected(button_type)
		return selected

	def delete(self, widget=None, group_name=''):
		del self.collection[group_ame]
		return True


def color_traversal(gui_stuff, color):
	# I'm thinking the stack requirements won't actually be that hefty, at least as long as we're talking about a hierarchy and not a proper graph ^_^
	if hasattr(gui_stuff, 'get_children'):
		for child in gui_stuff.get_children():
			color_traversal(child, color)
	if hasattr(gui_stuff, 'modify_fg'):
		gui_stuff.modify_fg(gtk.STATE_NORMAL, gtk.gdk.color_parse(color))


class generic_operation_gui_builder_class:
	across_color = 'darkblue'

	# group_list remains constant until the instance is destroyed, but the GUI stuff is recreated each time gimme_gui is called
	# This works because aggregate types are pass by reference in python
	def __init__(self, group_of_groups_list):
		# in practice, this will always be host groups and command groups (host_group and command_group, but they're dictionaries of hostgroups and
		# commandgroups)
		self.group_of_groups_list = group_of_groups_list

	def for_gui(self):
		# the whole schmear gets packed into this
		self.vbox = gtk.VBox()
		# just one of these
		self.vbox.pack_start(self.across_the_top(), expand=False, fill=False)
		# might put a separator here someday - maybe a double separator even
		for group_of_groups in self.group_of_groups_list:
			# one of these for each group_of_groups - IE, one for hosts, one for commands, and don't sort them, then loop on genericgroups
			result = gtk.Label('%s' % group_of_groups.name)
			result.show()
			self.vbox.pack_start(result, expand=False, fill=False)
			# we do sort genericgroups - they're things like "X11" or "Sears Singlecore"
			genericgroup_names = group_of_groups.keys()
			genericgroup_names.sort()
			scrolled_window = gtk.ScrolledWindow()
			scrolled_window.set_border_width(10)
			scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
			scrolled_window.show()
			box = gtk.VBox()
			scrolled_window.add_with_viewport(box)
			box.show()
			self.vbox.pack_start(scrolled_window, expand=True, fill=True)
			for genericgroup_name in genericgroup_names:
				genericgroup = group_of_groups[genericgroup_name]
				# maybe a separator here someday
				# one of these for each genericgroup, followed by the stuff for each item
				result = self.gui_for_genericgroup_proper(genericgroup)
				color_traversal(result, genericgroup.color)
				result.show()
				box.pack_start(result, expand=False, fill=False)
				# we do sort on item names - they're things like "xterm" or "node11"
				item_names = genericgroup.keys()
				item_names.sort()
				cols = 10
				# round up
				rows = (len(item_names) + cols - 1) / cols
				table = gtk.Table(cols, rows, False)
				table.show()
				item_number=0
				#print 'len(item_names)',len(item_names), 'rows',rows, 'cols',cols
				for item_name in item_names:
					item = genericgroup[item_name]
					# one of these for each item.  For now, we arrange them strictly vertically, but later we'll
					# most likely want a table of them, and it's possible the dimensions of that table should be
					# controlled somehow by the subclass
					result = self.gui_for_item(item, item_name)
					#print result
					color_traversal(result, genericgroup.color)
					#print result
					result.show()
					col = item_number / rows
					row = item_number % rows
					#print 'row',row, 'col',col
					table.attach(result.button, col, col+1, row, row+1)
					#box.pack_start(result.button, expand=False, fill=False)
					item_number += 1
				box.pack_start(table, expand=False, fill=False)
				# might put a separator here someday - at the end of the genericgroup
		self.vbox.show()
		return self.vbox


class select_gui_builder_class(generic_operation_gui_builder_class):
	shortname = 'Run'
	longname = 'Select host * command matchups and run them'

	###########################################
	# Start of across_the_top related methods #
	###########################################
	def stayopen_callback(self, widget, data=None):
		# intentionally doing nothing - we just interrogate the state of the button itself when needed
		pass
		return True

	def create_stayopen_button(self):
		# the stay open button
		if not hasattr(self, 'stayopen_button'):
			self.stayopen_button = gtk.ToggleButton('Stay open')
			set_button_color(self.stayopen_button, self.across_color)
			self.stayopen_button.set_tooltip_text("Keeps %s open after running a host x command matchup" % program_name)
			self.stayopen_button.connect("toggled", self.stayopen_callback)
			self.stayopen_button.show()
		return self.stayopen_button

	def deferred_callback(self, widget, data=None):
		if self.deferred_button.get_active():
			self.execute_button.set_sensitive(True)
		else:
			self.execute_button.set_sensitive(False)
			run_if_appropriate_conditions()
		return True

	def create_deferred_button(self):
		# the deferred button
		if not hasattr(self,'deferred_button'):
			self.deferred_button = gtk.ToggleButton('Deferred')
			set_button_color(self.deferred_button, self.across_color)
			self.deferred_button.set_tooltip_text("Run n commands on m hosts instead of the usual 1 x n or n x 1")
			self.deferred_button.connect("toggled", self.deferred_callback)
			self.deferred_button.show()
		return self.deferred_button

	def execute_callback(self, widget, data=None):
		# unconditionally - no need to run_if_appropriate_conditions()  ^_^  In fact, we -shouldn't- use run_if_appropriate_conditions()
		run_all_host_command_matches()
		return True

	def create_execute_button(self):
		# the execute button
		if not hasattr(self,'execute_button'):
			self.execute_button = gtk.Button('Execute')
			set_button_color(self.execute_button, self.across_color)
			self.execute_button.set_tooltip_text("Execute n commands on m hosts, after clicking the \"Deferred\" button")
			self.execute_button.connect("clicked", self.execute_callback)
		# if the deferred button exists, pay attention to it - otherwise, just assume this execute button should be insensitive
		if hasattr(self, 'deferred_button'):
			if self.deferred_button.get_active():
				self.execute_button.set_sensitive(True)
			else:
				self.execute_button.set_sensitive(False)
		else:
			self.execute_button.set_sensitive(False)
		return self.execute_button

	def create_command_repetition_spinner(self):
		# the multiple spinner
		if not hasattr(self,'command_repetition_spinner'):
			self.multiple_adj = gtk.Adjustment(1, 1, 9, 1, 1, 0)
			self.command_repetition_spinner = gtk.SpinButton(self.multiple_adj, 0, 0)
			self.command_repetition_spinner.set_digits(0)
			self.command_repetition_spinner.set_wrap(True)
			set_button_color(self.command_repetition_spinner, self.across_color)
			self.command_repetition_spinner.set_tooltip_text("Run the same host x command pairings multiple times")
		return self.command_repetition_spinner

	def create_seconds_between_commands_spinner(self):
		# the secs_between spinner
		if not hasattr(self,'seconds_between_commands_spinner'):
			self.secs_between_adj = gtk.Adjustment(2, 0, 30, 1, 1, 0)
			self.seconds_between_commands_spinner = gtk.SpinButton(self.secs_between_adj, 0, 0)
			self.seconds_between_commands_spinner.set_digits(0)
			self.seconds_between_commands_spinner.set_wrap(True)
			set_button_color(self.seconds_between_commands_spinner, self.across_color)
			self.seconds_between_commands_spinner.set_tooltip_text("Seconds between command invocations")
		return self.seconds_between_commands_spinner

	def for_across_the_top(self):
		if not hasattr(self, 'hbox'):
			self.hbox = gtk.HBox()
			# these widgets will be added to the "top row" from left to right with consistent coloring (at least for the buttons, that is - 
			# the spinners come up looking unchanged)
			for widget in [ \
				self.create_execute_button(), \
				self.create_deferred_button(), \
				self.create_stayopen_button(), \
#				self.create_deselect_all_button(), \
				self.create_seconds_between_commands_spinner(), \
				self.create_command_repetition_spinner(), \
				]:
				#set_button_color(widget, self.color)
				#self.hbox.pack_end(widget, spacing=10)
				self.hbox.pack_start(widget, expand=False, fill=False)
				widget.show()
		#color_traversal(self.hbox, 'pink')
		self.hbox.show()
		return self.hbox

	def across_the_top(self):
		self.widget = self.for_across_the_top()
		return self.widget

	###############################################################################################
	# End of across_the_top related methods, start of gui_for_genericgroup_proper related methods #
	###############################################################################################

	def gui_for_genericgroup_proper(self, genericgroup):
		vbox = gtk.VBox()
		hbox = gtk.HBox()
		vbox.pack_start(hbox, expand=False, fill=False)
		result = gtk.Label(genericgroup.name)
		result.show()
		hbox.pack_start(result, expand=False, fill=False)
		result = gtk.Button('Select')
		result.connect('clicked', lambda widget: genericgroup.mark_selected('Run', widget))
		result.show()
		hbox.pack_start(result, expand=False, fill=False)
		result = gtk.Button('Toggle')
		result.connect('clicked', lambda widget: genericgroup.toggle_selected('Run', widget))
		result.show()
		hbox.pack_start(result, expand=False, fill=False)
		result = gtk.Button('Deselect')
		result.connect('clicked', lambda widget: genericgroup.mark_deselected('Run', widget))
		result.show()
		hbox.pack_start(result, expand=False, fill=False)
		vbox.show()
		hbox.show()
		return vbox

	#############################################################################################
	# End of gui_for_genericgroup_proper related methods, start of gui_for_item related methods #
	#############################################################################################

	def gui_for_item(self, item, item_name):
		item.setup_gui_objects()
		return item.buttons['Run']


class generic_stub_operation_gui_builder_class:
	def __init__(self, group_of_groups_list):
		self.group_of_groups_list = group_of_groups_list

	def for_gui(self):
		self.vbox = gtk.VBox()
		self.vbox.show()
		label = gtk.Label('placeholder for %s' % self.longname)
		label.show()
		self.vbox.pack_start(label, expand=False, fill=False)
		label = gtk.Label('')
		label.show()
		self.vbox.pack_start(label, expand=False, fill=False)
		label=gtk.Label('Sorry, you cannot perform this operation via the GUI yet')
		label.show()
		self.vbox.pack_start(label, expand=False, fill=False)
		label = gtk.Label('')
		label.show()
		self.vbox.pack_start(label, expand=False, fill=False)
		if hasattr(self,'workaround'):
			label=gtk.Label('However you may be able to do the following at the command line:')
			label.show()
			self.vbox.pack_start(label, expand=False, fill=False)
			for line in string.splitfields(self.workaround, '\n'):
				label = gtk.Label(line)
				label.show()
				self.vbox.pack_start(label, expand=False, fill=False)
		return self.vbox


class modify_details_class(generic_stub_operation_gui_builder_class):
	shortname = 'modify'
	longname = 'modify the details of a host or command'
	workaround = 'There is not really a good workaround for this.\n'+ \
		'You are probably best off using a text editor and correcting lengths,\n'+ \
		'or waiting for this to be implemented.'


class add_item_class(generic_stub_operation_gui_builder_class):
	shortname = 'add item'
	longname = 'add a host to a preexisting hostgroup or command to a preexisting commandgroup'
	workaround = 'Use %s --add-host or %s --add-command.' % (program_name, program_name)


class delete_item_class(generic_stub_operation_gui_builder_class):
	shortname = 'delete item'
	longname = 'delete a host from a hostgroup or a command from a commandgroup'
	workaround = 'To delete a host:\n'+ \
		'cd ~/.hcm/hosts\n'+ \
		'cd hostgroupname\n'+ \
		'rm hostname'


class rename_class(generic_stub_operation_gui_builder_class):
	shortname = 'rename'
	longname = 'rename an item or group'
	workaround = 'To rename a commandgroup:\n'+ \
		'stop %s whether by exiting the GUI or with the kill command' % program_name + \
		'cd ~/.hcm/commands\n'+ \
		'mv oldcommandgroupname newcommandgroupname\n'+ \
		'\n'+ \
		'To rename a host\n'+ \
		'stop %s whether by exiting the GUI or with the kill command' % program_name + \
		'cd ~/.hcm/hosts\n'+ \
		'cd hostgroupname\n'+ \
		'mv oldhostname newhostname'


class merge_class(generic_stub_operation_gui_builder_class):
	shortname = 'merge'
	longname = 'merge items from one group to another'
	workaround = 'To merge hostgroups X and Y:\n'+ \
		'stop %s whether by exiting the GUI or with the kill command' % program_name + \
		'cd ~/.hcm/hosts\n'+ \
		'mv X/* Y/.\n'+ \
		'rmdir Y'


class split_class(generic_stub_operation_gui_builder_class):
	shortname = 'split'
	longname = 'split a group in two'
	workaround = 'To split hosts starting with a through m in hostgroups X into hostgroup Y:\n'+ \
		'stop %s whether by exiting the GUI or with the kill command' % program_name + \
		'cd ~/.hcm/hosts\n'+ \
		'mkdir Y\n'+ \
		'mv X/[a-m]* Y/.'


class add_group_class(generic_stub_operation_gui_builder_class):
	shortname = 'add group'
	longname = 'add a new hostgroup or commandgroup'
	workaround = 'To create a new hostgroup named H:\n'+ \
		'stop %s whether by exiting the GUI or with the kill command' % program_name + \
		'cd ~/.hcm/hosts\n'+ \
		'mkdir H'


class delete_group_class(generic_stub_operation_gui_builder_class):
	shortname = 'delete group'
	longname = 'delete a hostgroup or commandgroup'
	workaround = 'To delete a hostgroup named H:\n'+ \
		'stop %s whether by exiting the GUI or with the kill command' % program_name + \
		'cd ~/.hcm/hosts\n'+ \
		'rm -rf H'






# This function (and the myriad calls of it) probably should just go away altogether at some point, but I haven't really
# tested color_traversal yet ^_^
def set_button_color(button, color):
	#if hasattr(button, 'get_child'):
	#	button.get_child().modify_fg(gtk.STATE_NORMAL, gtk.gdk.color_parse(color))
	color_traversal(button, color)

# this should probably use gui_up to either give a CLI error or GUI error
def error_via_gui(self, message):
	dia = gtk.Dialog('%s error!' % program_name, gui.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)

# this should probably use gui_up to either give a CLI error or GUI error
def warn_via_gui(self, message):
	dia = gtk.Dialog('%s warning' % program_name, gui.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)


class always_top_row_class:
	color = 'black'

#	def deselect_callback(self, widget, data=None):
#		host_group.mark_deselected()
#		command_group.mark_deselected()
#		return True
#
#	def create_deselect_all_button(self):
#		# the deselect all button
#		if not hasattr(self, "deselect_all"):
#			self.deselect_all_button = gtk.Button('Deselect all')
#			set_button_color(self.deselect_all_button, self.color)
#			tooltips.set_tip(self.deselect_all_button, "Deselects all hosts and commands")
#			self.deselect_all_button.connect("clicked", self.deselect_callback)
#		return self.deselect_all_button

#	def add_group_callback(self):
#		sys.stderr.write('Not implemented\n')
#
#	def create_add_host_group_button(self):
#		# the add host group button
#		if not hasattr(self, 'add_host_group_button'):
#			self.add_host_group_button = gtk.Button('Add host group')
#			set_button_color(self.add_host_group_button, self.color)
#			self.add_host_group_button.connect("clicked", self.add_group_callback)
#		return self.add_host_group_button

#	def create_add_command_group_button(self):
#		# the add command group button
#		if not hasattr(self, 'add_command_group_button'):
#			self.add_command_group_button = gtk.Button('Add command group')
#			set_button_color(self.add_command_group_button, self.color)
#			self.add_command_group_button.connect("clicked", self.add_group_callback)
#		return self.add_command_group_button

	def create_exit_button(self):
		# the Exit button
		# someday this should save changes!
		if not hasattr(self, 'exit_button'):
			self.exit_button = gtk.Button('Exit')
			set_button_color(self.exit_button, self.color)
			self.exit_button.connect("clicked", lambda ignored_argument: save_changes_and_exit(ignored_argument))
			self.exit_button.set_tooltip_text("Save changes and terminate the program")
		return self.exit_button

	def create_quit_button(self):
		# the Quit button
		if not hasattr(self, 'quit_button'):
			self.quit_button = gtk.Button('Quit')
			set_button_color(self.quit_button, self.color)
			self.quit_button.connect("clicked", lambda w: sys.exit(0))
			self.quit_button.set_tooltip_text("Terminate the program without saving changes")
		return self.quit_button

	def about_callback(self, widget, data=None):
		dia = gtk.Dialog('About %s' % program_name, gui.window.get_toplevel(), \
			gtk.DIALOG_DESTROY_WITH_PARENT, ("Done", 123))
		dia.set_size_request(500, 300)
		for line in [ \
			'%s is by Dan Stromberg' % program_name, \
			'You are looking at version '+str(program_version), \
			'http://stromberg.dnsalias.org/~strombrg/%s/' % program_name, \
			'Please see the FAQ at http://stromberg.dnsalias.org/~strombrg/%s/FAQ.html' % program_name, \
			'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')
		return True

	def create_about_button(self):
		# the About button
		if not hasattr(self, 'about_button'):
			self.about_button = gtk.Button('About %s' % program_name)
			set_button_color(self.about_button, self.color)
			self.about_button.connect("clicked", self.about_callback)
		return self.about_button

	def help_callback(self, widget, data=None):
		dia = gtk.Dialog('%s help' % program_name, gui.window.get_toplevel(), \
			gtk.DIALOG_DESTROY_WITH_PARENT, ("Done", 123))
		dia.set_size_request(500, 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')
		return True
		
	def create_help_button(self):
		# the "Help" button
		if not hasattr(self, 'help_button'):
			self.help_button = gtk.Button('Help')
			self.help_button.connect("clicked", self.help_callback)
		return self.help_button

	def for_gui(self):
		if not hasattr(self, 'hbox'):
			self.hbox = gtk.HBox()
			# these widgets will be added to the "top row" from left to right with consistent coloring (at least for the buttons, that is - 
			# the spinners come up looking unchanged)
			for widget in [ \
				self.create_help_button(), \
				self.create_about_button(), \
				self.create_exit_button(), \
				self.create_quit_button(), \
				]:
				set_button_color(widget, self.color)
				#self.hbox.pack_end(widget, spacing=10)
				self.hbox.pack_start(widget, expand=False, fill=False)
				widget.show()
		self.hbox.show()
		return self.hbox

	def __init__(self):
		self.widget = self.for_gui()

	def show(self):
		self.widget.show()


class GUI:

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

		# Set the window title
		self.window.set_title(program_name)

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

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

		# set the dimensions of the enclosing window
		self.window.set_size_request(1270, 950)

		self.vbox = gtk.VBox()
		self.vbox.show()
		self.always_top_row = always_top_row_class()
		self.always_top_row.show()
		self.vbox.pack_start(self.always_top_row.widget, expand=False, fill=False)

		notebook = gtk.Notebook()
		notebook.show()
		self.builders = {}
		for builder_class in [ select_gui_builder_class, \
			modify_details_class, \
			add_item_class, \
			delete_item_class, \
			rename_class, \
			merge_class, \
			split_class, \
			add_group_class, \
			delete_group_class, ]:
			builder = builder_class([host_group, command_group])
			self.builders[builder.shortname] = builder
			frame = gtk.Frame(builder.longname)
			frame.add(builder.for_gui())
			frame.show()
			label = gtk.Label(builder.shortname)
			label.show()
			notebook.append_page(frame, label)
		self.vbox.pack_start(notebook, expand=True, fill=True)
			
		self.window.add(self.vbox)
		self.window.show()


def usage(retval):
	sys.stderr.write('Usage: %s\n' % program_name)
	sys.stderr.write('\t--add-host-group host_groupname\n')
	sys.stderr.write('\t--add-command-group command_groupname\n')
	sys.stderr.write('\t--add-host host_groupname hostname sshpath sshport\n')
	sys.stderr.write('\t--add-command command_groupname commandname command rununder\n')
	sys.stderr.write('\t--delete-host host_groupname hostname\n')
	sys.stderr.write('\t--delete-command command_groupname commandname\n')
	sys.stderr.write('\t--delete-host-group host_groupname\n')
	sys.stderr.write('\t--delete-command-group command_groupname\n')
	sys.stderr.write('\t--dump-host-groups\n')
	sys.stderr.write('\t--dump-command-groups\n')
	sys.stderr.write('\t--gui\n')
	sys.stderr.write('\t--ignore-already\n')
	sys.stderr.write('\t--already\n')
	sys.stderr.write('\t--help\n')
	sys.exit(retval)

def dump_group(group):
	group_keys = group.keys()
	group_keys.sort()
	for gk in group_keys:
		print gk, str(group[gk])

def add_group(group_type, group_class, item_class, group, group_name):
	if group.has_key(group_name):
		sys.stderr.write('%s: %s group %s already exists\n' % (sys.argv[0], group_type, group_name))
		if not ignore_already:
			sys.exit(1)
	else:
		group_directory = os.path.join(base_directory, '%ss' % group_type)
		ensure_directory(group_directory)
		#group[group_name] = group_class('%ss' % group_type, item_class, group_directory)
		group[group_name] = group_class(group_name, item_class, group_directory)

def add_item(item_type, item_class, group, group_to_add_to, item_to_add, *item_specific_parameters):
	if group.has_key(group_to_add_to):
		if group[group_to_add_to].hasitem(item_to_add):
			sys.stderr.write('%s: %s %s already exists in %s group %s\n' % (sys.argv[0], item_type, item_to_add, item_type, group_to_add_to))
			if not ignore_already:
				sys.exit(1)
		group_directory = os.path.join(base_directory, '%ss' % item_type)
		ensure_directory(group_directory)
		constructor_parameters = []
		constructor_parameters.append(os.path.join(os.path.join(group_directory, group_to_add_to), item_to_add))
		constructor_parameters.append(group_to_add_to)
		constructor_parameters.append(item_to_add)
		for i in list(item_specific_parameters):
			constructor_parameters.append(i)
		group[group_to_add_to].add_or_replace(item_to_add, apply(item_class, constructor_parameters))
	else:
		sys.stderr.write('%s: %s group %s does not exist.  Consider adding it with --add-%s-group\n' % \
			(program_name, item_type, group_to_add_to, item_type))
		sys.exit(1)

def delete_item(group_type, group, group_name_to_remove_from, item_name):
	if group.has_key(group_name_to_remove_from):
		group[group_name_to_remove_from].delete(item_name)
	else:
		sys.stderr.write('%s: %s group %s already nonexistent\n' % (sys.argv[0], group_type, group_name_to_remove_from))
		if not ignore_already:
			sys.exit(1)

def delete_group(group_type, group, group_name_to_remove):
	if group.has_key(group_name_to_remove):
		del group[group_name_to_remove]
	else:
		sys.stderr.write('%s: %s group %s already nonexistent\n' % (sys.argv[0], group_type, group_name_to_remove_from))
		if not ignore_already:
			sys.exit(1)

def drop_argv(n):
	for i in xrange(n):
		del sys.argv[1]

if __name__ == "__main__":
	# everything needs to read, so we always read (some things only need host groups or command groups though)
	host_group = group_of_groups_class('hosts', host_group_class, host_class, '~/.%s' % program_name)
	host_group.inhale()

	command_group = group_of_groups_class('commands', command_group_class, command_class, '~/.%s' % program_name)
	command_group.inhale()

	write_at_end=0
	ignore_already=0

	add_group_regex = re.compile('--add-([^-]*)-group')
	add_item_regex = re.compile('--add-([^-]*)')
	delete_item_regex = re.compile('--delete-([^-]*)')
	delete_group_regex = re.compile('--delete-([^-]*)-group')

	add_group_dict={}
	add_group_dict['host'] =		[host_group_class, host_class, host_group]
	add_group_dict['command'] =	[command_group_class, command_class, command_group]

	add_item_dict={}
	add_item_dict['host'] =			[host_class, host_group]
	add_item_dict['command'] =		[command_class, command_group]

	delete_item_dict={}
	delete_item_dict['host'] =		[host_group]
	delete_item_dict['command'] =	[command_group]

	delete_group_dict = delete_item_dict 

	#sys.stderr.write('foo\n')
	while sys.argv[1:]:
		if sys.argv[1] in ['--add-host-group', '--add-command-group']:
			group_type_match = add_group_regex.match(sys.argv[1])
			group_type = group_type_match.group(1)
			group_name_to_add = sys.argv[2]
			args = [group_type] + add_group_dict[group_type] + [group_name_to_add]
			apply(add_group, args)
			drop_argv(1)
			write_at_end = 1
		elif sys.argv[1] in ['--add-host', '--add-command']:
			item_type_match = add_item_regex.match(sys.argv[1])
			item_type = item_type_match.group(1)
			group_to_add_to = sys.argv[2]
			name_to_add = sys.argv[3]
			# in the case of an -add-host, argv[4] and argv[5] are the sshpath (bang path) and sshport
			# in the case of an -add-command, argv[4] and argv[5] are the full command and the rununder command
			args = [item_type] + add_item_dict[item_type] + [group_to_add_to, name_to_add, sys.argv[4], sys.argv[5]]
			apply(add_item, args)
			drop_argv(4)
			write_at_end = 1
		elif sys.argv[1] in ['--delete-host', '--delete-command']:
			delete_item_match = delete_item_regex.match(sys.argv[1])
			item_type = delete_item_match.group(1)
			group_name_to_delete_from = sys.argv[2]
			item_to_delete = sys.argv[3]
			args = [item_type] + delete_item_dict[item_type] + [group_name_to_delete_from, item_to_delete]
			apply(delete_item, args)
			drop_argv(2)
			write_at_end = 1
		elif sys.argv[1] in ['--delete-host-group', '--delete-command-group']:
			delete_group_match = delete_group_regex.match(sys.argv[1])
			group_type = delete_group_match.group(1)
			group_name_to_delete = sys.argv[2]
			args = [group_type] + delete_group_dict[group_type] + [group_name_to_delete]
			apply(delete_group, args)
			drop_argv(1)
			write_at_end = 1
		elif sys.argv[1] == '--dump-host-groups':
			dump_group(host_group)
		elif sys.argv[1] == '--dump-command-groups':
			dump_group(command_group)
		elif sys.argv[1] == '--gui':
			import pygtk
			pygtk.require('2.0')
			import gtk
			gui = GUI()
			gtk.main()
		elif sys.argv[1] == '--ignore-already':
			ignore_already=1
		elif sys.argv[1] == '--already':
			ignore_already=0
		elif sys.argv[1] in ['--help', '-h']:
			usage(0)
		else:
			sys.stderr.write('%s: Illegal argument: %s\n' % (sys.argv[0], sys.argv[1]))
			usage(1)
		drop_argv(1)
	#sys.stderr.write('bar\n')

	if write_at_end:
		save_changes_and_exit()
	else:
		sys.exit(0)