#!/usr/bin/env python
# ccc - a console chat client for asss
# <grelminar@yahoo.com>

# dist: public

import sys, time, curses, socket, select, os

ship_names = \
{
	0: 'Warbird',
	1: 'Javelin',
	2: 'Spider',
	3: 'Leviathan',
	4: 'Terrier',
	5: 'Weasel',
	6: 'Lancaster',
	7: 'Shark',
	8: 'Spectator'
}

class MyWindows:
	def __init__(me):
		me.stdscr = me.twin = me.iwin = me.pwin = None


class Bindings:
	def __init__(me):
		me.bindings = {}

		me.yes = 1
		me.no = 0
		me.true = 1
		me.false = 0

	def bind(me, seq, func, *args):
		if type(seq) == type(''):
			for c in range(1, 32):
				seq = seq.replace('^%c' % (c + ord('A') - 1), '%c' % c)
			seq = seq.replace('\\e', chr(27))
			seq = seq.replace('\\E', chr(27))
			seq = map(ord, seq)
		if type(seq) == type(0):
			seq = [seq]
		if type(seq) == type([]):
			seq = tuple(seq)

		def realfunc(func = func, args = args):
			func(*args)

		me.bindings[seq] = realfunc

	def getbinding(me, seq):
		if type(seq) == type(''):
			seq = map(ord, seq)
		seq = tuple(seq)
		return me.bindings.get(seq)


	# the possible functions
	def finish_line(me):
		global edit
		l = edit.gettext()
		edit.cleartext()
		if l:
			process_typed(l)

	def kill_line(me):
		global edit
		edit.cleartext()

	def backspace(me):
		global edit
		edit.backspace()

	def delete(me):
		global edit
		edit.delete()

	def curs_left(me):
		global edit
		edit.curs_left()

	def curs_right(me):
		global edit
		edit.curs_right()

	def curs_up(me):
		global edit
		edit.curs_up()

	def curs_down(me):
		global edit
		edit.curs_down()

	def curs_home(me):
		global edit
		edit.curs_home()

	def curs_end(me):
		global edit
		edit.curs_end()

	def scroll_up(me, lines = 1):
		global scrollback
		scrollback += lines

	def scroll_down(me, lines = 1):
		global scrollback
		scrollback -= lines
		if scrollback < 0:
			scrollback = 0

	def player_select_up(me):
		global players
		players.move_select_up()

	def player_select_down(me):
		global players
		players.move_select_down()

	def sort_player_list(me):
		global players
		players.set_sort()


class Line:
	def __init__(me, msg, color, tm = 0):
		me.msg = msg
		me.color = color
		if tm == 0:
			me.time = time.time()
		else:
			me.time = tm

		me.lastopts = None
		me.lastres = None

	def wordbreak(me, width, showtime = 0, indent = 2):

		if me.lastopts != (width, showtime, indent):
			me.lastopts = (width, showtime, indent)

			def totlen(wds):
				t = len(wds)
				for w in wds:
					t += len(w)
				return t

			words = me.msg.split(' ')
			lines = []
			if showtime:
				cl= [time.strftime('[%I:%M]', time.localtime(me.time))]
			else:
				cl = []

			for w in words:
				# too big to fit on a line alone
				while len(w) > width:
					# grab what can fit on this line
					if cl:
						i = width - totlen(cl) - 1
					else:
						i = width
					# add to this line, start new
					cl.append(w[:i])
					lines.append(cl)
					cl = [''] * indent
					w = w[i:]
				# if it will overflow this line
				if len(w) + totlen(cl) + 1 > width:
					lines.append(cl)
					cl = [''] * indent
				# now add it
				cl.append(w)

			# append last partial line
			lines.append(cl)

			me.lastres = map(lambda wds: ' '.join(wds), lines)

		return me.lastres


class Text:
	def __init__(me):
		me.lines = []

	def addline(me, l):
		global keeplines
		me.lines.append(l)
		if len(me.lines) > keeplines:
			me.lines = me.lines[-keeplines:]

	def display(me, scr):
		global scrollback, showtime, indent, newcrap

		# get window size
		h, w = scr.getmaxyx()

		rlines = []
		# only work on the last h lines
		for l in me.lines[-(h+scrollback):]:
			rlines.extend(map(lambda ln: (ln, l.color),
				l.wordbreak(w, showtime, indent)))
		rlines.reverse()
		# go that many lines up
		if scrollback >= len(rlines):
			scrollback = len(rlines) - 1
		scrollmark = (len(rlines)-1-scrollback)*(h-2)/(len(rlines))+1
		rlines = rlines[scrollback:]
		# and cut to the top of the window
		rlines = rlines[:h]

		# draw them
		y = h - 1
		scr.erase()
		for l, c in rlines:
			l = l.expandtabs()
			scr.addstr(y, 0, l, c)
			y = y - 1

		if scrollback < newcrap:
			newcrap = scrollback

		# display the "%d more" message at the bottom of the window
		if newcrap:
			s = '%d more' % newcrap
			scr.addstr(h-1, w-len(s)-1, s, curses.A_REVERSE)

		if scrollback:
			for i in range(1, h-1):
				scr.addch(i, w-1, curses.ACS_CKBOARD)
			scr.addch(scrollmark, w-1, curses.ACS_BULLET)
			# note, don't seem to be able to
			# scr.addstr(h-1,w-1,'A',curses.A_REVERSE)
			# it causes curses error.

		s = '%d lines' % len(me.lines)
		scr.addstr(0, w-len(s), s,curses.A_REVERSE)

		scr.noutrefresh()


class Edit:
	def __init__(me):
		me.line = []
		me.i = 0
		me.lastw = 0
		me.lastmax = 0

	def display(me, scr):
		h, w = scr.getmaxyx()
		me.lastw = w
		me.lastmax = w * h - 1
		if len(me.line) > me.lastmax:
			me.line = me.line[:me.lastmax]
		if me.i > me.lastmax:
			me.i = me.lastmax
		scr.erase()
		for c in me.line:
			scr.addch(c)
		scr.move(me.i / w, me.i % w)
		scr.noutrefresh()

	def gettext(me):
		return ''.join(map(chr, me.line))

	def is_double_colons(me):
		colon = ord(':')
		if not me.line or me.line[0] != colon:
			return 0
		if me.line[-1] == colon:
			return colon not in me.line[1:-1]
		else:
			return colon not in me.line[1:]

	def cleartext(me):
		me.__init__()

	def set_text(me, txt):
		me.line = map(ord, txt)
		# clip to max length
		me.line = me.line[0:me.lastmax]
		me.i = len(me.line)

	def get_text(me):
		return ''.join(map(chr, me.line))

	def backspace(me):
		if me.i > 0:
			me.line = me.line[:me.i-1] + me.line[me.i:]
			me.i -= 1

	def delete(me):
		if me.i < len(me.line):
			me.line = me.line[:me.i] + me.line[me.i+1:]

	def curs_left(me):
		if me.i > 0:
			me.i -= 1

	def curs_right(me):
		if me.i < len(me.line):
			me.i += 1

	def curs_up(me):
		if me.i > me.lastw:
			me.i -= me.lastw

	def curs_down(me):
		if me.i + me.lastw <= len(me.line):
			me.i += me.lastw

	def curs_home(me):
		me.i = 0
	def curs_end(me):
		me.i = len(me.line)

	def insert(me, ch):
		if len(me.line) < me.lastmax:
			me.line.insert(me.i, ch)
			me.i += 1


class Player:
	def __init__(me, name, freq):
		me.name = name
		me.freq = freq


class PlayerList:

	SORT_NONE = 0
	SORT_NAME = 1
	SORT_TEAM = 2

	def __init__(me):
		me.data = {}
		me.pos = 0
		me.offset = 0
		me.sorthow = PlayerList.SORT_NAME
		me.sorted = []
		me.sort()

	def clear(me):
		me.data = {}
		me.pos = 0
		me.offset = 0
		me.sort()

	def sort(me):
		# remember last selected player so we can select it again
		oldsel = me.get_selected()

		# created a sorted list
		if me.sorthow == PlayerList.SORT_NONE:
			me.sorted = me.data.values()
		elif me.sorthow == PlayerList.SORT_NAME:
			def mycmp(a, b):
				return cmp(a.name.lower(), b.name.lower())
			me.sorted = me.data.values()
			me.sorted.sort(mycmp)
		elif me.sorthow == PlayerList.SORT_TEAM:
			def mycmp(a, b):
				return 2 * cmp(a.freq, b.freq) + cmp(a.name.lower(), b.name.lower())
			tmp = me.data.values()
			tmp.sort(mycmp)

			# insert freq headers
			me.sorted = []
			lastfreq = -1
			for p in tmp:
				if p.freq != lastfreq:
					me.sorted.append('--- freq %d' % p.freq)
					lastfreq = p.freq
				me.sorted.append(p)
		else:
			me.sorted = []

		# try to select old selection, or if not, first real player in
		# list
		me.pos = -1
		idx = 0
		for p in me.sorted:
			if p is oldsel:
				me.pos = idx
				break
			if me.pos == -1 and type(p) != type(''):
				me.pos = idx
			idx = idx + 1

	def move_select_up(me):
		oldpos = me.pos
		me.pos = me.pos - 1
		while me.pos >= 0 and type(me.sorted[me.pos]) == type(''):
			me.pos = me.pos - 1
		if me.pos < 0:
			me.pos = oldpos

	def move_select_down(me):
		oldpos = me.pos
		me.pos = me.pos + 1
		while me.pos < len(me.sorted) and type(me.sorted[me.pos]) == type(''):
			me.pos = me.pos + 1
		if me.pos >= len(me.sorted):
			me.pos = oldpos

	def get_selected(me):
		if me.pos >= 0 and \
		   me.pos < len(me.sorted) and \
		   type(me.sorted[me.pos]) != type(''):
			return me.sorted[me.pos]
		else:
			return None

	def set_sort(me, sort = None):
		if sort is not None:
			me.sorthow = sort
		elif me.sorthow == PlayerList.SORT_NAME:
			me.sorthow = PlayerList.SORT_TEAM
		else:
			me.sorthow = PlayerList.SORT_NAME
		me.sort()

	def add(me, player):
		me.data[player.name] = player
		me.sort()

	def remove(me, name):
		del me.data[name]
		me.sort()

	def sfc(me, name, ship, freq):
		try:
			player = me.data[name]
		except:
			add_line("Unknown player name in ship change: '%s'" % name, colors.info)
			return

		player.ship = ship
		player.freq = freq
		me.sort()

	def display(me, scr):
		if not scr:
			return

		h, w = scr.getmaxyx()
		scr.erase()

		# update the scroll offset
		if me.pos < me.offset:
			me.offset = me.pos
		elif me.pos > (me.offset + h - 1):
			me.offset = me.pos - h + 1
		if me.offset >= len(me.sorted):
			me.offset = len(me.sorted) - 1
		if me.offset < 0:
			me.offset = 0

		if len(me.data) == 1:
			msg = '1 player'
		else:
			msg = '%d players' % len(me.data)
		msg = (msg + ' ' * w)[:w]
		scr.addstr(0, 0, msg, colors.pl_txt | curses.A_REVERSE)

		y = 1
		for thing in me.sorted[me.offset:]:
			if y >= h:
				break
			if type(thing) == type(''):
				scr.addstr(y, 0, thing[:w], colors.pl_txt)
			else:
				color = colors.pl_player
				if (y - 1 + me.offset) == me.pos:
					color = colors.pl_selected
				scr.addstr(y, 0, thing.name[:w], color)
			y = y + 1

		scr.noutrefresh()


class LastPrivList:
	def __init__(me, max = 5):
		me.list = []
		me.index = 0
		if not max:
			me.max = 5
		else:
			me.max = max

	def add(me, name):
		while name in me.list:
			me.list.remove(name)
		me.list[0:0] = [name]
		me.list = me.list[:me.max]

	def get(me, start = None):
		if start is not None:
			me.index = start
		if me.list:
			r = me.list[me.index]
			me.index = (me.index + 1) % len(me.list)
			return r
		else:
			return ''


class Colors:
	def __init__(me):
		from curses import init_pair, color_pair, A_BOLD, A_REVERSE, COLORS, COLOR_PAIRS

		me.colors = COLORS
		me.pairs = COLOR_PAIRS

		for c in range(1, me.pairs):
			init_pair(c, c % COLORS, 0)

		me.default = color_pair(0)
		me.info = color_pair(14 % me.pairs) | A_BOLD
		me.arena = color_pair(10 % me.pairs) | A_BOLD
		me.pub = color_pair(15 % me.pairs)
		me.priv = color_pair(2)
		me.rempriv = color_pair(10 % me.pairs)
		me.freq = color_pair(11 % me.pairs) | A_BOLD
		me.chat = color_pair(9 % me.pairs)
		me.mod = color_pair(12 % me.pairs)
		me.sysop = color_pair(9 % me.pairs) | A_BOLD

		me.pl_txt = color_pair(11 % me.pairs) | A_BOLD
		me.pl_player = color_pair(7 % me.pairs)
		me.pl_selected = color_pair(7 % me.pairs) | A_REVERSE

		# add colors that we couldn't do before
		global deferred_color_args
		for args in deferred_color_args:
			me.set_color(*args)
		del deferred_color_args

	def set_color(me, name, num, bold = 0):
		if bold:
			setattr(me, name, curses.color_pair(num) | curses.A_BOLD)
		else:
			setattr(me, name, curses.color_pair(num))


def init_logfile(fname):
	global logfile, original_log_filename
	import os.path

	original_log_filename = fname

	if not fname:
		logfile = 0
		return

	# support ~/ and strftime macros in the logfile setting
	fname = os.path.expanduser(fname)
	fname = time.strftime(fname)

	# if we're opening the same thing again, don't bother
	try:
		if logfile and logfile.name == fname:
			return
	except:
		pass

	logfile = 0

	try:
		logfile = open(fname, 'a', 1)
		add_line("Logging to file %s" % fname, 'info')
	except:
		print "Can't open '%s'. Logging disabled."

def reload_logfile():
	global original_log_filename
	init_logfile(original_log_filename)


# log types
L_INFO = 'I'
L_CMD = 'D'
L_PUB = 'P'
L_ARENA = 'A'
L_PRIV = 'V'
L_REMOTEPRIV = 'R'
L_FREQ = 'T' # team
L_NMEFREQ = 'E' # team
L_CHAT = 'C'
L_SYSOP = 'W' # warn
L_MOD = 'M' # warn

def log(type, text):
	if not logfile:
		return
	line = (time.asctime(), ' ', type, ' ', text, '\n')
	line = ''.join(line)
	logfile.write(line)


def start_curses():
	global wins

	wins.stdscr = curses.initscr()
	curses.cbreak()
	curses.noecho()
	wins.stdscr.keypad(1)
	wins.stdscr.nodelay(1)
	curses.start_color()
	init_wins()


def init_wins():
	global wins, editheight, pwidth

	h, w = wins.stdscr.getmaxyx()

	wins.stdscr.clear()

	wins.iwin = curses.newwin(editheight, w, h-editheight, 0)
	wins.stdscr.hline(h-editheight-1, 0, curses.ACS_HLINE, w)

	if pwidth:
		wins.twin = curses.newwin(h-editheight-1, w-pwidth-1, 0, 0)
		wins.pwin = curses.newwin(h-editheight-1, pwidth, 0, w-pwidth)
		wins.stdscr.vline(0, w-pwidth-1, curses.ACS_VLINE, h-editheight-1)
		wins.stdscr.addch(h-editheight-1, w-pwidth-1, curses.ACS_BTEE)
	else:
		wins.twin = curses.newwin(h-editheight-1, w, 0, 0)

	wins.stdscr.refresh()


def end_curses():
	global wins

	wins.stdscr.keypad(0)
	curses.echo()
	curses.nocbreak()
	curses.endwin()


def process_typed(line):
	global myname, colors, showtime, breakloop

	if not line:
		return

	def send_priv(dest, msg):
		send_line("SEND:PRIV:%s:%s" % (dest, msg))
		txt = "%s to %s> %s" % (myname, dest, msg)
		add_line(txt, colors.priv)
		log(L_PRIV, txt)

	if line.startswith(';'):
		# chat message
		ns = line.find(';', 1)
		if ns > 0 and ns < 16:
			chan = line[1:ns]
			try:
				int(chan)
				msg = line[ns+1:]
			except:
				chan = '1'
				msg = line[1:]
		else:
			chan = '1'
			msg = line[1:]

		send_line("SEND:CHAT:%s" % line[1:])
		txt = "%s:%s> %s" % (chan, myname, msg)
		add_line(txt, colors.chat)
		log(L_CHAT, txt)
	elif line.startswith("'") or line.startswith('//') or line.startswith('"'):
		# team chat
		pass
	elif line.startswith(':'):
		# priv msg
		ns = line.find(':', 1)
		if ns != -1:
			send_priv(line[1:ns], line[ns+1:])
	elif line.startswith('/'):
		# priv msg
		dest = players.get_selected()
		if dest:
			send_priv(dest.name, line[1:])
	elif line.startswith('\\'):
		# mod chat
		send_line("SEND:MOD:%s" % line[1:])
		txt = "%s> %s" % (myname, line[1:])
		add_line(txt, colors.mod)
		log(L_MOD, txt)
	elif line.startswith('?'):
		# possible command
		cmd = line[1:]
		if cmd.startswith('exit') or cmd.startswith('quit'):
			breakloop = 1
		elif cmd.startswith('showtime'):
			showtime = 1
		elif cmd.startswith('hidetime'):
			showtime = 0
		elif cmd.startswith('go '):
			send_line("GO:%s" % cmd[3:])
		elif cmd.startswith('namesort'):
			order = cmd[8:].strip()
			if order == 'name':
				players.set_sort(PlayerList.SORT_NAME)
			elif order == 'team':
				players.set_sort(PlayerList.SORT_TEAM)
			elif order == 'none':
				players.set_sort(PlayerList.SORT_NONE)
			else:
				players.set_sort()
		else:
			send_line("SEND:PUB:%s" % line)
		txt = "%s> %s" % (myname, line)
		add_line(txt, colors.default)
		log(L_CMD, txt)
	elif line.startswith('='):
		# freq change
		try:
			freq = int(line[1:])
			send_line("CHANGEFREQ:%d" % freq)
		except:
			add_line("Bad freq number", colors.info)
	else:
		# pub msg
		send_line("SEND:PUB:%s" % line)
		txt = "%s> %s" % (myname, line)
		add_line(txt, colors.pub)
		log(L_PUB, txt)


def process_loginok(line):
	global myname, logindone
	logindone = ("Login ok.", 1)
	myname = line


def process_loginbad(line):
	global logindone
	logindone = ("Bad login: %s" % line, 0)


def process_inarena(line):
	global players, myname, myfreq

	arena, freq = line.split(':')
	myfreq = int(freq)
	txt = "Entered arena: %s" % arena
	add_line(txt, 'info')
	log(L_INFO, txt)

	players.clear()
	players.add(Player(myname, myfreq))


def process_player(line):
	global players

	name, ship, freq = line.split(':')
	txt = "Player here: %s" % name
	add_line(txt, 'info')
	log(L_INFO, txt)
	players.add(Player(name, int(freq)))


def process_entering(line):
	global players

	name, ship, freq = line.split(':')
	txt = "Player entering: %s" % name
	add_line(txt, 'info')
	log(L_INFO, txt)
	players.add(Player(name, int(freq)))


def process_leaving(line):
	global players

	name = line

	txt = "Player leaving: %s" % name
	add_line(txt, 'info')
	log(L_INFO, txt)
	players.remove(name)


def process_msg(line):
	global colors, last_priv_list

	type, rest = line.split(':', 1)
	if type == 'ARENA':
		msg = rest
		add_line(msg, colors.arena)
		log(L_ARENA, msg)
	elif type == 'PUB':
		name, msg = rest.split(':', 1)
		txt = '%s> %s' % (name, msg)
		add_line(txt, colors.pub)
		log(L_PUB, txt)
	elif type == 'PRIV':
		name, msg = rest.split(':', 1)
		txt = '%s> %s' % (name, msg)
		add_line(txt, colors.priv)
		last_priv_list.add(name)
		log(L_PRIV, txt)
	elif type == 'FREQ':
		name, msg = rest.split(':', 1)
		txt = '%s> %s' % (name, msg)
		add_line(txt, colors.freq)
		log(L_FREQ, txt)
	elif type == 'CHAT':
		msg = rest
		add_line(msg, colors.chat)
		log(L_CHAT, msg)
	elif type == 'MOD':
		name, msg = rest.split(':', 1)
		txt = '%s> %s' % (name, msg)
		add_line(txt, colors.mod)
		log(L_MOD, txt)
	elif type == 'SYSOP':
		msg = rest
		add_line(msg, colors.sysop)
		log(L_SYSOP, msg)
	else:
		add_line("Bad message subtype from server: '%s'" % type, colors.info)


def process_sfc(line):
	global colors, players

	player, ship, freq = line.split(':')
	ship = int(ship)
	freq = int(freq)

	players.sfc(player, ship, freq)

	if ship == 8:
		txt = '%s is now on freq %s as a spectator' % (player, freq)
		add_line(txt, colors.info)
		log(L_INFO, txt)
	else:
		txt = '%s is now on freq %s in a %s' % (player, freq, ship_names[ship])
		add_line(txt, colors.info)
		log(L_INFO, txt)


dispatch = \
{
	'LOGINOK': process_loginok,
	'LOGINBAD': process_loginbad,
	'INARENA': process_inarena,
	'PLAYER': process_player,
	'ENTERING': process_entering,
	'LEAVING': process_leaving,
	'MSG': process_msg,
	'SHIPFREQCHANGE': process_sfc
}

def process_incoming(line):
	type, rest = line.split(':', 1)
	if dispatch.has_key(type):
		dispatch[type](rest)
	else:
		add_line("Bad message type from server: '%s'" % type, colors.info)


def send_line(line):
	global sock
	sock.sendall(line + '\n')


deferred_lines = []
def add_line(line, color = None):
	global text, colors, scrollback, jumpback, newcrap
	if not colors:
		# if we're trying to add a line before curses initialization,
		# save it in a list for later.
		global deferred_lines
		deferred_lines.append((line, color))
		return
	if color == None:
		color = colors.default
	elif type(color) == type(''):
		color = getattr(colors, color)
	text.addline(Line(line, color))
	if jumpback:
		scrollback = 0
	elif scrollback > 0:
		newcrap += 1
		scrollback += 1


def complete_deferred_lines():
	global deferred_lines
	for l, c in deferred_lines:
		add_line(l, c)
	del deferred_lines


colonstate = 0
def process_key():
	global wins, bindings, edit, colonstate
	colon = ord(':')

	ch = wins.stdscr.getch()

	if ch != colon:
		colonstate = 0

	if ch == -1:
		pass
	elif ch == curses.KEY_RESIZE:
		init_wins()
	elif ch == curses.KEY_MOUSE:
		id, x, y, z, state = curses.getmouse()
		if state & curses.BUTTON4_CLICKED:
			pass
	elif ch == 27:
		# get all chars of an escape sequence
		seq = []
		while ch != -1:
			seq.append(ch)
			ch = wins.stdscr.getch()
		f = bindings.getbinding(seq)
		if f:
			f()
	elif ch == colon:
		if colonstate == 0:
			if edit.get_text() == ':':
				colonstate = 1
				edit.set_text(':%s:' % last_priv_list.get(0))
			else:
				edit.insert(ch)
		elif colonstate == 1:
			edit.set_text(':%s:' % last_priv_list.get())
	else:
		# hand off to bindings
		f = bindings.getbinding([ch])
		if f:
			f()
		elif ch >= 32 and ch < 127:
			edit.insert(ch)


inbuf = ''
def read_data():
	global sock, inbuf

	try:
		r = sock.recv(1024)
		if r:
			inbuf = inbuf + r
		else:
			raise "Server disconnected!"

		lines = inbuf.splitlines(1)
		inbuf = ''
		for l in lines:
			if l.endswith('\n') or l.endswith('\r'):
				process_incoming(l.strip())
			else:
				inbuf = l
		del lines

	except:
		pass


last_sent_keepalive = time.time()
def send_keepalive(tm):
	global keepalive, last_sent_keepalive

	if keepalive and (tm - last_sent_keepalive) > keepalive:
		send_line('NOOP')
		last_sent_keepalive = tm


# reload the log file once per day
last_days_value = 0
def check_logfile(tm):
	global last_days_value
	timetuple = time.localtime(tm)
	days = timetuple[2]
	if days != last_days_value:
		reload_logfile()
		last_days_value = days


def main_loop():
	global wins, text, edit, sock, breakloop

	breakloop = 0

	while not breakloop:
		# show stuff
		text.display(wins.twin)
		players.display(wins.pwin)
		edit.display(wins.iwin)
		curses.doupdate()

		# do a select
		try:
			select.select([sys.stdin, sock], [], [], 10)
		except:
			pass

		tm = time.time()

		# get key
		process_key()
		# read some data
		read_data()
		# send keepalive
		send_keepalive(tm)
		# check for re-opening logfile
		check_logfile(tm)


def cleanup_code(code):
	import string
	lines = code.split('\n')
	lines = map(string.strip, lines)
	return '\n'.join(lines)


deferred_color_args = []
def process_config(file):
	global bindings
	global editheight, pwidth, showtime, indent, jumpback, keeplines
	global keepalive

	bindings = Bindings()

	# default bindings
	bindings.bind('^J', bindings.finish_line)
	bindings.bind(curses.KEY_ENTER, bindings.finish_line)
	bindings.bind(curses.KEY_BACKSPACE, bindings.backspace)
	bindings.bind(curses.KEY_DC, bindings.delete)
	bindings.bind(curses.KEY_RIGHT, bindings.curs_right)
	bindings.bind(curses.KEY_LEFT, bindings.curs_left)
	bindings.bind('^A', bindings.curs_home)
	bindings.bind(curses.KEY_HOME, bindings.curs_home)
	bindings.bind(curses.KEY_END, bindings.curs_end)
	bindings.bind('^E', bindings.curs_end)
	bindings.bind(curses.KEY_UP, bindings.scroll_up, 1)
	bindings.bind(curses.KEY_DOWN, bindings.scroll_down, 1)
	bindings.bind('^U', bindings.kill_line)

	bindings.bind(curses.KEY_PPAGE, bindings.player_select_up)
	bindings.bind(curses.KEY_NPAGE, bindings.player_select_down)
	bindings.bind(curses.KEY_F2, bindings.sort_player_list)

	opts = {}

	# get 'bind' and the action funcs in there
	for d in dir(Bindings):
		opts[d] = getattr(bindings, d)

	# get the KEY_* constants in there
	for d in dir(curses):
		if d.startswith('KEY_'):
			opts[d] = getattr(curses, d)

	# add a color setter function
	def color(*args):
		global deferred_color_args
		deferred_color_args.append(args)
	opts['color'] = color

	execfile(file, opts, opts)

	# check command-line params
	args = sys.argv[1:]
	if args:
		for arg in args:
			if opts.has_key(arg):
				try:
					exec cleanup_code(opts[arg]) in opts
				except:
					import traceback
					print "Error in config file:"
					traceback.print_exc()
			else:
				try:
					exec arg
				except:
					import traceback
					print "Can't process command line argument '%s':" % arg
					traceback.print_exc()
	elif opts.has_key('default'):
		try:
			exec cleanup_code(opts['default']) in opts
		except:
			import traceback
			print "Error in config file:"
			traceback.print_exc()

	editheight = opts.get('editheight', 3)
	pwidth = opts.get('playerwidth', 15)
	indent = opts.get('indent', 2)
	showtime = opts.get('showtime', 1)
	jumpback = opts.get('jumpback', 0)
	keeplines = opts.get('keeplines', 1000)
	keepalive = opts.get('keepalive', 0)

	return opts


def main():
	global wins, text, edit, sock, players, myname, colors, scrollback
	global newcrap, logindone, last_priv_list

	colors = 0
	myname = ''
	myfreq = 0
	players = PlayerList()

	# get config file
	if os.environ.get('CCC_CONFIG_FILE'):
		opts = process_config(os.environ.get('CCC_CONFIG_FILE'))
	else:
		try:
			opts = process_config(os.environ.get('HOME') + '/.ccc-config')
		except:
			import traceback
			print "Error in config file:"
			traceback.print_exc()
			sys.exit(1)

	# check for required options
	if not opts.has_key('name') or not opts.has_key('password') or \
	   not opts.has_key('server') or not opts.has_key('port'):
		print "The config file must include a name, password, server, and port."
		sys.exit(2)

	last_priv_list = LastPrivList(opts.get('playerhistory'))

	scrollback = 0
	newcrap = 0

	wins = MyWindows()
	text = Text()
	edit = Edit()

	print "ccc - a console chat client <grelminar@yahoo.com>"
	print "Connecting to %s:%d as %s..." % \
		(opts['server'], opts['port'], opts['name'])

	sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	sock.connect((opts['server'], opts['port']))
	sock.setblocking(0)

	print "Logging in..."

	send_line("LOGIN:1:%s:%s" % (opts['name'], opts['password']))

	# limited version of main loop just to get login result
	logindone = 0
	while not logindone:
		try:
			select.select([sock], [], [], 1000)
		except:
			pass

		read_data()

	msg, ok = logindone
	print msg

	if not ok:
		return

	init_logfile(opts.get('logfile'))

	txt = "Connected to %s:%d as %s" % (opts['server'], opts['port'], myname)
	add_line(txt, 'info')
	log(L_INFO, txt)

	# enter arena
	send_line("GO:%s" % opts.get('arena', ''))

	# join chats and other autocommands
	if opts.has_key('chats'):
		send_line("SEND:CMD:?chat=%s" % opts['chats'])

	start_curses()

	try:
		# default bindings based on curses settings. these can't be done
		# until after we init curses.
		bindings.bind(curses.erasechar(), bindings.backspace)
		bindings.bind(curses.killchar(), bindings.kill_line)

		colors = Colors()

		# curses initialization is done, add lines that we couldn't add
		# before.
		complete_deferred_lines()

		# init lines can be split by newlines or bars
		if opts.has_key('init'):
			for l1 in opts['init'].split('\n'):
				for l2 in l1.split('|'):
					process_typed(l2)

		main_loop()

	finally:
		end_curses()

	log(L_INFO, "Disconnected.")
	print "Disconnected."


if __name__ == '__main__':
	main()

