summary refs log tree commit diff stats
path: root/src/common/text.c
diff options
context:
space:
mode:
authorberkeviktor@aol.com <berkeviktor@aol.com>2011-02-24 04:14:30 +0100
committerberkeviktor@aol.com <berkeviktor@aol.com>2011-02-24 04:14:30 +0100
commit4a6ceffb98a0b785494f680d3776c4bfc4052f9e (patch)
tree850703c1c841ccd99f58d0b06084615aaebe782c /src/common/text.c
parentf16af8be941b596dedac3bf4e371ee2d21f4b598 (diff)
add xchat r1489
Diffstat (limited to 'src/common/text.c')
-rw-r--r--src/common/text.c2309
1 files changed, 2309 insertions, 0 deletions
diff --git a/src/common/text.c b/src/common/text.c
new file mode 100644
index 00000000..a2198517
--- /dev/null
+++ b/src/common/text.c
@@ -0,0 +1,2309 @@
+/* X-Chat
+ * Copyright (C) 1998 Peter Zelezny.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
+ */
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <ctype.h>
+#include <time.h>
+#include <sys/types.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <sys/mman.h>
+
+#include "xchat.h"
+#include <glib/ghash.h>
+#include "cfgfiles.h"
+#include "chanopt.h"
+#include "plugin.h"
+#include "fe.h"
+#include "server.h"
+#include "util.h"
+#include "outbound.h"
+#include "xchatc.h"
+#include "text.h"
+#ifdef WIN32
+#include <windows.h>
+#endif
+
+struct pevt_stage1
+{
+	int len;
+	char *data;
+	struct pevt_stage1 *next;
+};
+
+
+static void mkdir_p (char *dir);
+static char *log_create_filename (char *channame);
+
+
+static char *
+scrollback_get_filename (session *sess, char *buf, int max)
+{
+	char *net, *chan;
+
+	net = server_get_network (sess->server, FALSE);
+	if (!net)
+		return NULL;
+
+	snprintf (buf, max, "%s/scrollback/%s/%s.txt", get_xdir_fs (), net, "");
+	mkdir_p (buf);
+
+	chan = log_create_filename (sess->channel);
+	snprintf (buf, max, "%s/scrollback/%s/%s.txt", get_xdir_fs (), net, chan);
+	free (chan);
+
+	return buf;
+}
+
+#if 0
+
+static void
+scrollback_unlock (session *sess)
+{
+	char buf[1024];
+
+	if (scrollback_get_filename (sess, buf, sizeof (buf) - 6) == NULL)
+		return;
+
+	strcat (buf, ".lock");
+	unlink (buf);
+}
+
+static gboolean
+scrollback_lock (session *sess)
+{
+	char buf[1024];
+	int fh;
+
+	if (scrollback_get_filename (sess, buf, sizeof (buf) - 6) == NULL)
+		return FALSE;
+
+	strcat (buf, ".lock");
+
+	if (access (buf, F_OK) == 0)
+		return FALSE;	/* can't get lock */
+
+	fh = open (buf, O_CREAT | O_TRUNC | O_APPEND | O_WRONLY, 0644);
+	if (fh == -1)
+		return FALSE;
+
+	return TRUE;
+}
+
+#endif
+
+void
+scrollback_close (session *sess)
+{
+	if (sess->scrollfd != -1)
+	{
+		close (sess->scrollfd);
+		sess->scrollfd = -1;
+	}
+}
+
+static char *
+file_to_buffer (char *file, int *len)
+{
+	int fh;
+	char *buf;
+	struct stat st;
+
+	fh = open (file, O_RDONLY | OFLAGS);
+	if (fh == -1)
+		return NULL;
+
+	fstat (fh, &st);
+
+	buf = malloc (st.st_size);
+	if (!buf)
+	{
+		close (fh);
+		return NULL;
+	}
+
+	if (read (fh, buf, st.st_size) != st.st_size)
+	{
+		free (buf);
+		close (fh);
+		return NULL;
+	}
+
+	*len = st.st_size;
+	close (fh);
+	return buf;
+}
+
+/* shrink the file to roughly prefs.max_lines */
+
+static void
+scrollback_shrink (session *sess)
+{
+	char file[1024];
+	char *buf;
+	int fh;
+	int lines;
+	int line;
+	int len;
+	char *p;
+
+	scrollback_close (sess);
+	sess->scrollwritten = 0;
+	lines = 0;
+
+	if (scrollback_get_filename (sess, file, sizeof (file)) == NULL)
+		return;
+
+	buf = file_to_buffer (file, &len);
+	if (!buf)
+		return;
+
+	/* count all lines */
+	p = buf;
+	while (p != buf + len)
+	{
+		if (*p == '\n')
+			lines++;
+		p++;
+	}
+
+	fh = open (file, O_CREAT | O_TRUNC | O_APPEND | O_WRONLY, 0644);
+	if (fh == -1)
+	{
+		free (buf);
+		return;
+	}
+
+	line = 0;
+	p = buf;
+	while (p != buf + len)
+	{
+		if (*p == '\n')
+		{
+			line++;
+			if (line >= lines - prefs.max_lines &&
+				 p + 1 != buf + len)
+			{
+				p++;
+				write (fh, p, len - (p - buf));
+				break;
+			}
+		}
+		p++;
+	}
+
+	close (fh);
+	free (buf);
+}
+
+static void
+scrollback_save (session *sess, char *text)
+{
+	char buf[512 * 4];
+	time_t stamp;
+	int len;
+
+	if (sess->type == SESS_SERVER)
+		return;
+
+	if (sess->text_scrollback == SET_DEFAULT)
+	{
+		if (!prefs.text_replay)
+			return;
+	}
+	else
+	{
+		if (sess->text_scrollback != SET_ON)
+			return;
+	}
+
+	if (sess->scrollfd == -1)
+	{
+		if (scrollback_get_filename (sess, buf, sizeof (buf)) == NULL)
+			return;
+
+		sess->scrollfd = open (buf, O_CREAT | O_APPEND | O_WRONLY, 0644);
+		if (sess->scrollfd == -1)
+			return;
+	}
+
+	stamp = time (0);
+	if (sizeof (stamp) == 4)	/* gcc will optimize one of these out */
+		write (sess->scrollfd, buf, snprintf (buf, sizeof (buf), "T %d ", (int)stamp));
+	else
+		write (sess->scrollfd, buf, snprintf (buf, sizeof (buf), "T %"G_GINT64_FORMAT" ", (gint64)stamp));
+
+	len = strlen (text);
+	write (sess->scrollfd, text, len);
+	if (len && text[len - 1] != '\n')
+		write (sess->scrollfd, "\n", 1);
+
+	sess->scrollwritten++;
+
+	if ((sess->scrollwritten * 2 > prefs.max_lines && prefs.max_lines > 0) ||
+       sess->scrollwritten > 32000)
+		scrollback_shrink (sess);
+}
+
+void
+scrollback_load (session *sess)
+{
+	int fh;
+	char buf[512 * 4];
+	char *text;
+	time_t stamp;
+	int lines;
+	char *map, *end_map;
+	struct stat statbuf;
+	const char *begin, *eol;
+
+	if (sess->text_scrollback == SET_DEFAULT)
+	{
+		if (!prefs.text_replay)
+			return;
+	}
+	else
+	{
+		if (sess->text_scrollback != SET_ON)
+			return;
+	}
+
+	if (scrollback_get_filename (sess, buf, sizeof (buf)) == NULL)
+		return;
+
+	fh = open (buf, O_RDONLY | OFLAGS);
+	if (fh == -1)
+		return;
+
+	if (fstat (fh, &statbuf) < 0)
+		return;
+
+	map = mmap (NULL, statbuf.st_size, PROT_READ, MAP_PRIVATE, fh, 0);
+	if (map == MAP_FAILED)
+		return;
+
+	end_map = map + statbuf.st_size;
+	
+	lines = 0;
+	begin = map;
+	while (begin < end_map)
+	{
+		int n_bytes;
+
+		eol = memchr (begin, '\n', end_map - begin);
+
+		if (!eol)
+			eol = end_map;
+
+		n_bytes = MIN (eol - begin, sizeof (buf) - 1);
+		
+		strncpy (buf, begin, n_bytes);
+
+		buf[n_bytes] = 0;
+		
+		if (buf[0] == 'T')
+		{
+			if (sizeof (time_t) == 4)
+				stamp = strtoul (buf + 2, NULL, 10);
+			else
+				stamp = strtoull (buf + 2, NULL, 10); /* just incase time_t is 64 bits */
+			text = strchr (buf + 3, ' ');
+			if (text)
+			{
+				text = strip_color (text + 1, -1, STRIP_COLOR);
+				fe_print_text (sess, text, stamp);
+				g_free (text);
+			}
+			lines++;
+		}
+
+		begin = eol + 1;
+	}
+
+	sess->scrollwritten = lines;
+
+	if (lines)
+	{
+		text = ctime (&stamp);
+		text[24] = 0;	/* get rid of the \n */
+		snprintf (buf, sizeof (buf), "\n*\t%s %s\n\n", _("Loaded log from"), text);
+		fe_print_text (sess, buf, 0);
+		/*EMIT_SIGNAL (XP_TE_GENMSG, sess, "*", buf, NULL, NULL, NULL, 0);*/
+	}
+
+	munmap (map, statbuf.st_size);
+	close (fh);
+}
+
+void
+log_close (session *sess)
+{
+	char obuf[512];
+	time_t currenttime;
+
+	if (sess->logfd != -1)
+	{
+		currenttime = time (NULL);
+		write (sess->logfd, obuf,
+			 snprintf (obuf, sizeof (obuf) - 1, _("**** ENDING LOGGING AT %s\n"),
+						  ctime (&currenttime)));
+		close (sess->logfd);
+		sess->logfd = -1;
+	}
+}
+
+static void
+mkdir_p (char *dir)	/* like "mkdir -p" from a shell, FS encoding */
+{
+	char *start = dir;
+
+	/* the whole thing already exists? */
+	if (access (dir, F_OK) == 0)
+		return;
+
+	while (*dir)
+	{
+#ifdef WIN32
+		if (dir != start && (*dir == '/' || *dir == '\\'))
+#else
+		if (dir != start && *dir == '/')
+#endif
+		{
+			*dir = 0;
+#ifdef WIN32
+			mkdir (start);
+#else
+			mkdir (start, S_IRUSR | S_IWUSR | S_IXUSR);
+#endif
+			*dir = '/';
+		}
+		dir++;
+	}
+}
+
+static char *
+log_create_filename (char *channame)
+{
+	char *tmp, *ret;
+	int mbl;
+
+	ret = tmp = strdup (channame);
+	while (*tmp)
+	{
+		mbl = g_utf8_skip[((unsigned char *)tmp)[0]];
+		if (mbl == 1)
+		{
+#ifndef WIN32
+			*tmp = rfc_tolower (*tmp);
+			if (*tmp == '/')
+#else
+			/* win32 can't handle filenames with \|/><:"*? characters */
+			if (*tmp == '\\' || *tmp == '|' || *tmp == '/' ||
+				 *tmp == '>'  || *tmp == '<' || *tmp == ':' ||
+				 *tmp == '\"' || *tmp == '*' || *tmp == '?')
+#endif
+				*tmp = '_';
+		}
+		tmp += mbl;
+	}
+
+	return ret;
+}
+
+/* like strcpy, but % turns into %% */
+
+static char *
+log_escape_strcpy (char *dest, char *src, char *end)
+{
+	while (*src)
+	{
+		*dest = *src;
+		if (dest + 1 == end)
+			break;
+		dest++;
+		src++;
+
+		if (*src == '%')
+		{
+			if (dest + 1 == end)
+				break;
+			dest[0] = '%';
+			dest++;
+		}
+	}
+
+	dest[0] = 0;
+	return dest - 1;
+}
+
+/* substitutes %c %n %s into buffer */
+
+static void
+log_insert_vars (char *buf, int bufsize, char *fmt, char *c, char *n, char *s)
+{
+	char *end = buf + bufsize;
+
+	while (1)
+	{
+		switch (fmt[0])
+		{
+		case 0:
+			buf[0] = 0;
+			return;
+
+		case '%':
+			fmt++;
+			switch (fmt[0])
+			{
+			case 'c':
+				buf = log_escape_strcpy (buf, c, end);
+				break;
+			case 'n':
+				buf = log_escape_strcpy (buf, n, end);
+				break;
+			case 's':
+				buf = log_escape_strcpy (buf, s, end);
+				break;
+			default:
+				buf[0] = '%';
+				buf++;
+				buf[0] = fmt[0];
+				break;
+			}
+			break;
+
+		default:
+			buf[0] = fmt[0];
+		}
+		fmt++;
+		buf++;
+		/* doesn't fit? */
+		if (buf == end)
+		{
+			buf[-1] = 0;
+			return;
+		}
+	}
+}
+
+static char *
+log_create_pathname (char *servname, char *channame, char *netname)
+{
+	char fname[384];
+	char fnametime[384];
+	char *fs;
+	struct tm *tm;
+	time_t now;
+
+	if (!netname)
+		netname = "NETWORK";
+
+	/* first, everything is in UTF-8 */
+	if (!rfc_casecmp (channame, servname))
+		channame = strdup ("server");
+	else
+		channame = log_create_filename (channame);
+	log_insert_vars (fname, sizeof (fname), prefs.logmask, channame, netname, servname);
+	free (channame);
+
+	/* insert time/date */
+	now = time (NULL);
+	tm = localtime (&now);
+	strftime (fnametime, sizeof (fnametime), fname, tm);
+
+	/* create final path/filename */
+#ifdef WIN32
+	if (fnametime[0] == '/' || (fnametime[0] >= 'A' && fnametime[1] == ':'))
+#else
+	if (fnametime[0] == '/')	/* is it fullpath already? */
+#endif
+		snprintf (fname, sizeof (fname), "%s", fnametime);
+	else
+		snprintf (fname, sizeof (fname), "%s/xchatlogs/%s", get_xdir_utf8 (), fnametime);
+
+	/* now we need it in FileSystem encoding */
+	fs = xchat_filename_from_utf8 (fname, -1, 0, 0, 0);
+
+	/* create all the subdirectories */
+	if (fs)
+		mkdir_p (fs);
+
+	return fs;
+}
+
+static int
+log_open_file (char *servname, char *channame, char *netname)
+{
+	char buf[512];
+	int fd;
+	char *file;
+	time_t currenttime;
+
+	file = log_create_pathname (servname, channame, netname);
+	if (!file)
+		return -1;
+
+#ifdef WIN32
+	fd = open (file, O_CREAT | O_APPEND | O_WRONLY, S_IREAD|S_IWRITE);
+#else
+	fd = open (file, O_CREAT | O_APPEND | O_WRONLY, 0644);
+#endif
+	g_free (file);
+
+	if (fd == -1)
+		return -1;
+	currenttime = time (NULL);
+	write (fd, buf,
+			 snprintf (buf, sizeof (buf), _("**** BEGIN LOGGING AT %s\n"),
+						  ctime (&currenttime)));
+
+	return fd;
+}
+
+static void
+log_open (session *sess)
+{
+	static gboolean log_error = FALSE;
+
+	log_close (sess);
+	sess->logfd = log_open_file (sess->server->servername, sess->channel,
+										  server_get_network (sess->server, FALSE));
+
+	if (!log_error && sess->logfd == -1)
+	{
+		char message[512];
+		snprintf (message, sizeof (message),
+					_("* Can't open log file(s) for writing. Check the\n" \
+					  "  permissions on %s/xchatlogs"), get_xdir_utf8 ());
+		fe_message (message, FE_MSG_WAIT | FE_MSG_ERROR);
+
+		log_error = TRUE;
+	}
+}
+
+void
+log_open_or_close (session *sess)
+{
+	if (sess->text_logging == SET_DEFAULT)
+	{
+		if (prefs.logging)
+			log_open (sess);
+		else
+			log_close (sess);
+	}
+	else
+	{
+		if (sess->text_logging)
+			log_open (sess);
+		else
+			log_close (sess);
+	}
+}
+
+int
+get_stamp_str (char *fmt, time_t tim, char **ret)
+{
+	char *loc = NULL;
+	char dest[128];
+	gsize len;
+
+	/* strftime wants the format string in LOCALE! */
+	if (!prefs.utf8_locale)
+	{
+		const gchar *charset;
+
+		g_get_charset (&charset);
+		loc = g_convert_with_fallback (fmt, -1, charset, "UTF-8", "?", 0, 0, 0);
+		if (loc)
+			fmt = loc;
+	}
+
+	len = strftime (dest, sizeof (dest), fmt, localtime (&tim));
+	if (len)
+	{
+		if (prefs.utf8_locale)
+			*ret = g_strdup (dest);
+		else
+			*ret = g_locale_to_utf8 (dest, len, 0, &len, 0);
+	}
+
+	if (loc)
+		g_free (loc);
+
+	return len;
+}
+
+static void
+log_write (session *sess, char *text)
+{
+	char *temp;
+	char *stamp;
+	char *file;
+	int len;
+
+	if (sess->text_logging == SET_DEFAULT)
+	{
+		if (!prefs.logging)
+			return;
+	}
+	else
+	{
+		if (sess->text_logging != SET_ON)
+			return;
+	}
+
+	if (sess->logfd == -1)
+		log_open (sess);
+
+	/* change to a different log file? */
+	file = log_create_pathname (sess->server->servername, sess->channel,
+										 server_get_network (sess->server, FALSE));
+	if (file)
+	{
+		if (access (file, F_OK) != 0)
+		{
+			close (sess->logfd);
+			sess->logfd = log_open_file (sess->server->servername, sess->channel,
+												  server_get_network (sess->server, FALSE));
+		}
+		g_free (file);
+	}
+
+	if (prefs.timestamp_logs)
+	{
+		len = get_stamp_str (prefs.timestamp_log_format, time (0), &stamp);
+		if (len)
+		{
+			write (sess->logfd, stamp, len);
+			g_free (stamp);
+		}
+	}
+	temp = strip_color (text, -1, STRIP_ALL);
+	len = strlen (temp);
+	write (sess->logfd, temp, len);
+	/* lots of scripts/plugins print without a \n at the end */
+	if (temp[len - 1] != '\n')
+		write (sess->logfd, "\n", 1);	/* emulate what xtext would display */
+	g_free (temp);
+}
+
+/* converts a CP1252/ISO-8859-1(5) hybrid to UTF-8                           */
+/* Features: 1. It never fails, all 00-FF chars are converted to valid UTF-8 */
+/*           2. Uses CP1252 in the range 80-9f because ISO doesn't have any- */
+/*              thing useful in this range and it helps us receive from mIRC */
+/*           3. The five undefined chars in CP1252 80-9f are replaced with   */
+/*              ISO-8859-15 control codes.                                   */
+/*           4. Handles 0xa4 as a Euro symbol ala ISO-8859-15.               */
+/*           5. Uses ISO-8859-1 (which matches CP1252) for everything else.  */
+/*           6. This routine measured 3x faster than g_convert :)            */
+
+static unsigned char *
+iso_8859_1_to_utf8 (unsigned char *text, int len, gsize *bytes_written)
+{
+	unsigned int idx;
+	unsigned char *res, *output;
+	static const unsigned short lowtable[] = /* 74 byte table for 80-a4 */
+	{
+	/* compressed utf-8 table: if the first byte's 0x20 bit is set, it
+	   indicates a 2-byte utf-8 sequence, otherwise prepend a 0xe2. */
+		0x82ac, /* 80 Euro. CP1252 from here on... */
+		0xe281, /* 81 NA */
+		0x809a, /* 82 */
+		0xe692, /* 83 */
+		0x809e, /* 84 */
+		0x80a6, /* 85 */
+		0x80a0, /* 86 */
+		0x80a1, /* 87 */
+		0xeb86, /* 88 */
+		0x80b0, /* 89 */
+		0xe5a0, /* 8a */
+		0x80b9, /* 8b */
+		0xe592, /* 8c */
+		0xe28d, /* 8d NA */
+		0xe5bd, /* 8e */
+		0xe28f, /* 8f NA */
+		0xe290, /* 90 NA */
+		0x8098, /* 91 */
+		0x8099, /* 92 */
+		0x809c, /* 93 */
+		0x809d, /* 94 */
+		0x80a2, /* 95 */
+		0x8093, /* 96 */
+		0x8094, /* 97 */
+		0xeb9c, /* 98 */
+		0x84a2, /* 99 */
+		0xe5a1, /* 9a */
+		0x80ba, /* 9b */
+		0xe593, /* 9c */
+		0xe29d, /* 9d NA */
+		0xe5be, /* 9e */
+		0xe5b8, /* 9f */
+		0xe2a0, /* a0 */
+		0xe2a1, /* a1 */
+		0xe2a2, /* a2 */
+		0xe2a3, /* a3 */
+		0x82ac  /* a4 ISO-8859-15 Euro. */
+	};
+
+	if (len == -1)
+		len = strlen (text);
+
+	/* worst case scenario: every byte turns into 3 bytes */
+	res = output = g_malloc ((len * 3) + 1);
+	if (!output)
+		return NULL;
+
+	while (len)
+	{
+		if (G_LIKELY (*text < 0x80))
+		{
+			*output = *text;	/* ascii maps directly */
+		}
+		else if (*text <= 0xa4)	/* 80-a4 use a lookup table */
+		{
+			idx = *text - 0x80;
+			if (lowtable[idx] & 0x2000)
+			{
+				*output++ = (lowtable[idx] >> 8) & 0xdf; /* 2 byte utf-8 */
+				*output = lowtable[idx] & 0xff;
+			}
+			else
+			{
+				*output++ = 0xe2;	/* 3 byte utf-8 */
+				*output++ = (lowtable[idx] >> 8) & 0xff;
+				*output = lowtable[idx] & 0xff;
+			}
+		}
+		else if (*text < 0xc0)
+		{
+			*output++ = 0xc2;
+			*output = *text;
+		}
+		else
+		{
+			*output++ = 0xc3;
+			*output = *text - 0x40;
+		}
+		output++;
+		text++;
+		len--;
+	}
+	*output = 0;	/* terminate */
+	*bytes_written = output - res;
+
+	return res;
+}
+
+char *
+text_validate (char **text, int *len)
+{
+	char *utf;
+	gsize utf_len;
+
+	/* valid utf8? */
+	if (g_utf8_validate (*text, *len, 0))
+		return NULL;
+
+#ifdef WIN32
+	if (GetACP () == 1252) /* our routine is better than iconv's 1252 */
+#else
+	if (prefs.utf8_locale)
+#endif
+		/* fallback to iso-8859-1 */
+		utf = iso_8859_1_to_utf8 (*text, *len, &utf_len);
+	else
+	{
+		/* fallback to locale */
+		utf = g_locale_to_utf8 (*text, *len, 0, &utf_len, NULL);
+		if (!utf)
+			utf = iso_8859_1_to_utf8 (*text, *len, &utf_len);
+	}
+
+	if (!utf) 
+	{
+		*text = g_strdup ("%INVALID%");
+		*len = 9;
+	} else
+	{
+		*text = utf;
+		*len = utf_len;
+	}
+
+	return utf;
+}
+
+void
+PrintText (session *sess, char *text)
+{
+	char *conv;
+
+	if (!sess)
+	{
+		if (!sess_list)
+			return;
+		sess = (session *) sess_list->data;
+	}
+
+	/* make sure it's valid utf8 */
+	if (text[0] == 0)
+	{
+		text = "\n";
+		conv = NULL;
+	} else
+	{
+		int len = -1;
+		conv = text_validate ((char **)&text, &len);
+	}
+
+	log_write (sess, text);
+	scrollback_save (sess, text);
+	fe_print_text (sess, text, 0);
+
+	if (conv)
+		g_free (conv);
+}
+
+void
+PrintTextf (session *sess, char *format, ...)
+{
+	va_list args;
+	char *buf;
+
+	va_start (args, format);
+	buf = g_strdup_vprintf (format, args);
+	va_end (args);
+
+	PrintText (sess, buf);
+	g_free (buf);
+}
+
+/* Print Events stuff here --AGL */
+
+/* Consider the following a NOTES file:
+
+   The main upshot of this is:
+   * Plugins and Perl scripts (when I get round to signaling perl.c) can intercept text events and do what they like
+   * The default text engine can be config'ed
+
+   By default it should appear *exactly* the same (I'm working hard not to change the default style) but if you go into Settings->Edit Event Texts you can change the text's. The format is thus:
+
+   The normal %Cx (color) and %B (bold) etc work
+
+   $x is replaced with the data in var x (e.g. $1 is often the nick)
+
+   $axxx is replace with a single byte of value xxx (in base 10)
+
+   AGL (990507)
+ */
+
+/* These lists are thus:
+   pntevts_text[] are the strings the user sees (WITH %x etc)
+   pntevts[] are the data strings with \000 etc
+ */
+
+/* To add a new event:
+
+   Think up a name (like "Join")
+   Make up a pevt_name_help struct
+	Add an entry to textevents.in
+	Type: make textevents
+ */
+
+/* Internals:
+
+   On startup ~/.xchat/printevents.conf is loaded if it doesn't exist the
+   defaults are loaded. Any missing events are filled from defaults.
+   Each event is parsed by pevt_build_string and a binary output is produced
+   which looks like:
+
+   (byte) value: 0 = {
+   (int) numbers of bytes
+   (char []) that number of byte to be memcpy'ed into the buffer
+   }
+   1 =
+   (byte) number of varable to insert
+   2 = end of buffer
+
+   Each XP_TE_* signal is hard coded to call text_emit which calls
+   display_event which decodes the data
+
+   This means that this system *should be faster* than snprintf because
+   it always 'knows' that format of the string (basically is preparses much
+   of the work)
+
+   --AGL
+ */
+
+char *pntevts_text[NUM_XP];
+char *pntevts[NUM_XP];
+
+#define pevt_generic_none_help NULL
+
+static char * const pevt_genmsg_help[] = {
+	N_("Left message"),
+	N_("Right message"),
+};
+
+static char * const pevt_join_help[] = {
+	N_("The nick of the joining person"),
+	N_("The channel being joined"),
+	N_("The host of the person"),
+};
+
+static char * const pevt_chanaction_help[] = {
+	N_("Nickname"),
+	N_("The action"),
+	N_("Mode char"),
+	N_("Identified text"),
+};
+
+static char * const pevt_chanmsg_help[] = {
+	N_("Nickname"),
+	N_("The text"),
+	N_("Mode char"),
+	N_("Identified text"),
+};
+
+static char * const pevt_privmsg_help[] = {
+	N_("Nickname"),
+	N_("The message"),
+	N_("Identified text")
+};
+
+static char * const pevt_changenick_help[] = {
+	N_("Old nickname"),
+	N_("New nickname"),
+};
+
+static char * const pevt_newtopic_help[] = {
+	N_("Nick of person who changed the topic"),
+	N_("Topic"),
+	N_("Channel"),
+};
+
+static char * const pevt_topic_help[] = {
+	N_("Channel"),
+	N_("Topic"),
+};
+
+static char * const pevt_kick_help[] = {
+	N_("The nickname of the kicker"),
+	N_("The person being kicked"),
+	N_("The channel"),
+	N_("The reason"),
+};
+
+static char * const pevt_part_help[] = {
+	N_("The nick of the person leaving"),
+	N_("The host of the person"),
+	N_("The channel"),
+};
+
+static char * const pevt_chandate_help[] = {
+	N_("The channel"),
+	N_("The time"),
+};
+
+static char * const pevt_topicdate_help[] = {
+	N_("The channel"),
+	N_("The creator"),
+	N_("The time"),
+};
+
+static char * const pevt_quit_help[] = {
+	N_("Nick"),
+	N_("Reason"),
+	N_("Host"),
+};
+
+static char * const pevt_pingrep_help[] = {
+	N_("Who it's from"),
+	N_("The time in x.x format (see below)"),
+};
+
+static char * const pevt_notice_help[] = {
+	N_("Who it's from"),
+	N_("The message"),
+};
+
+static char * const pevt_channotice_help[] = {
+	N_("Who it's from"),
+	N_("The Channel it's going to"),
+	N_("The message"),
+};
+
+static char * const pevt_uchangenick_help[] = {
+	N_("Old nickname"),
+	N_("New nickname"),
+};
+
+static char * const pevt_ukick_help[] = {
+	N_("The person being kicked"),
+	N_("The channel"),
+	N_("The nickname of the kicker"),
+	N_("The reason"),
+};
+
+static char * const pevt_partreason_help[] = {
+	N_("The nick of the person leaving"),
+	N_("The host of the person"),
+	N_("The channel"),
+	N_("The reason"),
+};
+
+static char * const pevt_ctcpsnd_help[] = {
+	N_("The sound"),
+	N_("The nick of the person"),
+	N_("The channel"),
+};
+
+static char * const pevt_ctcpgen_help[] = {
+	N_("The CTCP event"),
+	N_("The nick of the person"),
+};
+
+static char * const pevt_ctcpgenc_help[] = {
+	N_("The CTCP event"),
+	N_("The nick of the person"),
+	N_("The Channel it's going to"),
+};
+
+static char * const pevt_chansetkey_help[] = {
+	N_("The nick of the person who set the key"),
+	N_("The key"),
+};
+
+static char * const pevt_chansetlimit_help[] = {
+	N_("The nick of the person who set the limit"),
+	N_("The limit"),
+};
+
+static char * const pevt_chanop_help[] = {
+	N_("The nick of the person who did the op'ing"),
+	N_("The nick of the person who has been op'ed"),
+};
+
+static char * const pevt_chanhop_help[] = {
+	N_("The nick of the person who has been halfop'ed"),
+	N_("The nick of the person who did the halfop'ing"),
+};
+
+static char * const pevt_chanvoice_help[] = {
+	N_("The nick of the person who did the voice'ing"),
+	N_("The nick of the person who has been voice'ed"),
+};
+
+static char * const pevt_chanban_help[] = {
+	N_("The nick of the person who did the banning"),
+	N_("The ban mask"),
+};
+
+static char * const pevt_chanrmkey_help[] = {
+	N_("The nick who removed the key"),
+};
+
+static char * const pevt_chanrmlimit_help[] = {
+	N_("The nick who removed the limit"),
+};
+
+static char * const pevt_chandeop_help[] = {
+	N_("The nick of the person of did the deop'ing"),
+	N_("The nick of the person who has been deop'ed"),
+};
+static char * const pevt_chandehop_help[] = {
+	N_("The nick of the person of did the dehalfop'ing"),
+	N_("The nick of the person who has been dehalfop'ed"),
+};
+
+static char * const pevt_chandevoice_help[] = {
+	N_("The nick of the person of did the devoice'ing"),
+	N_("The nick of the person who has been devoice'ed"),
+};
+
+static char * const pevt_chanunban_help[] = {
+	N_("The nick of the person of did the unban'ing"),
+	N_("The ban mask"),
+};
+
+static char * const pevt_chanexempt_help[] = {
+	N_("The nick of the person who did the exempt"),
+	N_("The exempt mask"),
+};
+
+static char * const pevt_chanrmexempt_help[] = {
+	N_("The nick of the person removed the exempt"),
+	N_("The exempt mask"),
+};
+
+static char * const pevt_chaninvite_help[] = {
+	N_("The nick of the person who did the invite"),
+	N_("The invite mask"),
+};
+
+static char * const pevt_chanrminvite_help[] = {
+	N_("The nick of the person removed the invite"),
+	N_("The invite mask"),
+};
+
+static char * const pevt_chanmodegen_help[] = {
+	N_("The nick of the person setting the mode"),
+	N_("The mode's sign (+/-)"),
+	N_("The mode letter"),
+	N_("The channel it's being set on"),
+};
+
+static char * const pevt_whois1_help[] = {
+	N_("Nickname"),
+	N_("Username"),
+	N_("Host"),
+	N_("Full name"),
+};
+
+static char * const pevt_whois2_help[] = {
+	N_("Nickname"),
+	N_("Channel Membership/\"is an IRC operator\""),
+};
+
+static char * const pevt_whois3_help[] = {
+	N_("Nickname"),
+	N_("Server Information"),
+};
+
+static char * const pevt_whois4_help[] = {
+	N_("Nickname"),
+	N_("Idle time"),
+};
+
+static char * const pevt_whois4t_help[] = {
+	N_("Nickname"),
+	N_("Idle time"),
+	N_("Signon time"),
+};
+
+static char * const pevt_whois5_help[] = {
+	N_("Nickname"),
+	N_("Away reason"),
+};
+
+static char * const pevt_whois6_help[] = {
+	N_("Nickname"),
+};
+
+static char * const pevt_whoisid_help[] = {
+	N_("Nickname"),
+	N_("Message"),
+	"Numeric"
+};
+
+static char * const pevt_whoisauth_help[] = {
+	N_("Nickname"),
+	N_("Message"),
+	N_("Account"),
+};
+
+static char * const pevt_whoisrealhost_help[] = {
+	N_("Nickname"),
+	N_("Real user@host"),
+	N_("Real IP"),
+	N_("Message"),
+};
+
+static char * const pevt_generic_channel_help[] = {
+	N_("Channel Name"),
+};
+
+static char * const pevt_servertext_help[] = {
+	N_("Text"),
+	N_("Server Name"),
+	N_("Raw Numeric or Identifier")
+};
+
+static char * const pevt_sslmessage_help[] = {
+	N_("Text"),
+	N_("Server Name")
+};
+
+static char * const pevt_invited_help[] = {
+	N_("Channel Name"),
+	N_("Nick of person who invited you"),
+	N_("Server Name"),
+};
+
+static char * const pevt_usersonchan_help[] = {
+	N_("Channel Name"),
+	N_("Users"),
+};
+
+static char * const pevt_nickclash_help[] = {
+	N_("Nickname in use"),
+	N_("Nick being tried"),
+};
+
+static char * const pevt_connfail_help[] = {
+	N_("Error"),
+};
+
+static char * const pevt_connect_help[] = {
+	N_("Host"),
+	N_("IP"),
+	N_("Port"),
+};
+
+static char * const pevt_sconnect_help[] = {
+	"PID"
+};
+
+static char * const pevt_generic_nick_help[] = {
+	N_("Nickname"),
+	N_("Server Name"),
+	N_("Network")
+};
+
+static char * const pevt_chanmodes_help[] = {
+	N_("Channel Name"),
+	N_("Modes string"),
+};
+
+static char * const pevt_rawmodes_help[] = {
+	N_("Nickname"),
+	N_("Modes string"),
+};
+
+static char * const pevt_kill_help[] = {
+	N_("Nickname"),
+	N_("Reason"),
+};
+
+static char * const pevt_dccchaterr_help[] = {
+	N_("Nickname"),
+	N_("IP address"),
+	N_("Port"),
+	N_("Error"),
+};
+
+static char * const pevt_dccstall_help[] = {
+	N_("DCC Type"),
+	N_("Filename"),
+	N_("Nickname"),
+};
+
+static char * const pevt_generic_file_help[] = {
+	N_("Filename"),
+	N_("Error"),
+};
+
+static char * const pevt_dccrecverr_help[] = {
+	N_("Filename"),
+	N_("Destination filename"),
+	N_("Nickname"),
+	N_("Error"),
+};
+
+static char * const pevt_dccrecvcomp_help[] = {
+	N_("Filename"),
+	N_("Destination filename"),
+	N_("Nickname"),
+	N_("CPS"),
+};
+
+static char * const pevt_dccconfail_help[] = {
+	N_("DCC Type"),
+	N_("Nickname"),
+	N_("Error"),
+};
+
+static char * const pevt_dccchatcon_help[] = {
+	N_("Nickname"),
+	N_("IP address"),
+};
+
+static char * const pevt_dcccon_help[] = {
+	N_("Nickname"),
+	N_("IP address"),
+	N_("Filename"),
+};
+
+static char * const pevt_dccsendfail_help[] = {
+	N_("Filename"),
+	N_("Nickname"),
+	N_("Error"),
+};
+
+static char * const pevt_dccsendcomp_help[] = {
+	N_("Filename"),
+	N_("Nickname"),
+	N_("CPS"),
+};
+
+static char * const pevt_dccoffer_help[] = {
+	N_("Filename"),
+	N_("Nickname"),
+	N_("Pathname"),
+};
+
+static char * const pevt_dccfileabort_help[] = {
+	N_("Nickname"),
+	N_("Filename")
+};
+
+static char * const pevt_dccchatabort_help[] = {
+	N_("Nickname"),
+};
+
+static char * const pevt_dccresumeoffer_help[] = {
+	N_("Nickname"),
+	N_("Filename"),
+	N_("Position"),
+};
+
+static char * const pevt_dccsendoffer_help[] = {
+	N_("Nickname"),
+	N_("Filename"),
+	N_("Size"),
+	N_("IP address"),
+};
+
+static char * const pevt_dccgenericoffer_help[] = {
+	N_("DCC String"),
+	N_("Nickname"),
+};
+
+static char * const pevt_notifynumber_help[] = {
+	N_("Number of notify items"),
+};
+
+static char * const pevt_serverlookup_help[] = {
+	N_("Server Name"),
+};
+
+static char * const pevt_servererror_help[] = {
+	N_("Text"),
+};
+
+static char * const pevt_foundip_help[] = {
+	N_("IP"),
+};
+
+static char * const pevt_dccrename_help[] = {
+	N_("Old Filename"),
+	N_("New Filename"),
+};
+
+static char * const pevt_ctcpsend_help[] = {
+	N_("Receiver"),
+	N_("Message"),
+};
+
+static char * const pevt_ignoreaddremove_help[] = {
+	N_("Hostmask"),
+};
+
+static char * const pevt_resolvinguser_help[] = {
+	N_("Nickname"),
+	N_("Hostname"),
+};
+
+static char * const pevt_malformed_help[] = {
+	N_("Nickname"),
+	N_("The Packet"),
+};
+
+static char * const pevt_pingtimeout_help[] = {
+	N_("Seconds"),
+};
+
+static char * const pevt_uinvite_help[] = {
+	N_("Nick of person who have been invited"),
+	N_("Channel Name"),
+	N_("Server Name"),
+};
+
+static char * const pevt_banlist_help[] = {
+	N_("Channel"),
+	N_("Banmask"),
+	N_("Who set the ban"),
+	N_("Ban time"),
+};
+
+static char * const pevt_discon_help[] = {
+	N_("Error"),
+};
+
+#include "textevents.h"
+
+static void
+pevent_load_defaults ()
+{
+	int i;
+
+	for (i = 0; i < NUM_XP; i++)
+	{
+		if (pntevts_text[i])
+			free (pntevts_text[i]);
+
+		/* make-te.c sets this 128 flag (DON'T call gettext() flag) */
+		if (te[i].num_args & 128)
+			pntevts_text[i] = strdup (te[i].def);
+		else
+			pntevts_text[i] = strdup (_(te[i].def));
+	}
+}
+
+void
+pevent_make_pntevts ()
+{
+	int i, m;
+	char out[1024];
+
+	for (i = 0; i < NUM_XP; i++)
+	{
+		if (pntevts[i] != NULL)
+			free (pntevts[i]);
+		if (pevt_build_string (pntevts_text[i], &(pntevts[i]), &m) != 0)
+		{
+			snprintf (out, sizeof (out),
+						 _("Error parsing event %s.\nLoading default."), te[i].name);
+			fe_message (out, FE_MSG_WARN);
+			free (pntevts_text[i]);
+			/* make-te.c sets this 128 flag (DON'T call gettext() flag) */
+			if (te[i].num_args & 128)
+				pntevts_text[i] = strdup (te[i].def);
+			else
+				pntevts_text[i] = strdup (_(te[i].def));
+			if (pevt_build_string (pntevts_text[i], &(pntevts[i]), &m) != 0)
+			{
+				fprintf (stderr,
+							"XChat CRITICAL *** default event text failed to build!\n");
+				abort ();
+			}
+		}
+	}
+}
+
+/* Loading happens at 2 levels:
+   1) File is read into blocks
+   2) Pe block is parsed and loaded
+
+   --AGL */
+
+/* Better hope you pass good args.. --AGL */
+
+static void
+pevent_trigger_load (int *i_penum, char **i_text, char **i_snd)
+{
+	int penum = *i_penum, len;
+	char *text = *i_text, *snd = *i_snd;
+
+	if (penum != -1 && text != NULL)
+	{
+		len = strlen (text) + 1;
+		if (pntevts_text[penum])
+			free (pntevts_text[penum]);
+		pntevts_text[penum] = malloc (len);
+		memcpy (pntevts_text[penum], text, len);
+	}
+
+	if (text)
+		free (text);
+	if (snd)
+		free (snd);
+	*i_text = NULL;
+	*i_snd = NULL;
+	*i_penum = 0;
+}
+
+static int
+pevent_find (char *name, int *i_i)
+{
+	int i = *i_i, j;
+
+	j = i + 1;
+	while (1)
+	{
+		if (j == NUM_XP)
+			j = 0;
+		if (strcmp (te[j].name, name) == 0)
+		{
+			*i_i = j;
+			return j;
+		}
+		if (j == i)
+			return -1;
+		j++;
+	}
+}
+
+int
+pevent_load (char *filename)
+{
+	/* AGL, I've changed this file and pevent_save, could you please take a look at
+	 *      the changes and possibly modify them to suit you
+	 *      //David H
+	 */
+	char *buf, *ibuf;
+	int fd, i = 0, pnt = 0;
+	struct stat st;
+	char *text = NULL, *snd = NULL;
+	int penum = 0;
+	char *ofs;
+
+	if (filename == NULL)
+		fd = xchat_open_file ("pevents.conf", O_RDONLY, 0, 0);
+	else
+		fd = xchat_open_file (filename, O_RDONLY, 0, XOF_FULLPATH);
+
+	if (fd == -1)
+		return 1;
+	if (fstat (fd, &st) != 0)
+		return 1;
+	ibuf = malloc (st.st_size);
+	read (fd, ibuf, st.st_size);
+	close (fd);
+
+	while (buf_get_line (ibuf, &buf, &pnt, st.st_size))
+	{
+		if (buf[0] == '#')
+			continue;
+		if (strlen (buf) == 0)
+			continue;
+
+		ofs = strchr (buf, '=');
+		if (!ofs)
+			continue;
+		*ofs = 0;
+		ofs++;
+		/*if (*ofs == 0)
+			continue;*/
+
+		if (strcmp (buf, "event_name") == 0)
+		{
+			if (penum >= 0)
+				pevent_trigger_load (&penum, &text, &snd);
+			penum = pevent_find (ofs, &i);
+			continue;
+		} else if (strcmp (buf, "event_text") == 0)
+		{
+			if (text)
+				free (text);
+
+#if 0
+			/* This allows updating of old strings. We don't use new defaults
+				if the user has customized the strings (.e.g a text theme).
+				Hash of the old default is enough to identify and replace it.
+				This only works in English. */
+
+			switch (g_str_hash (ofs))
+			{
+			case 0x526743a4:
+		/* %C08,02 Hostmask                  PRIV NOTI CHAN CTCP INVI UNIG %O */
+				text = strdup (te[XP_TE_IGNOREHEADER].def);
+				break;
+
+			case 0xe91bc9c2:
+		/* %C08,02                                                         %O */
+				text = strdup (te[XP_TE_IGNOREFOOTER].def);
+				break;
+
+			case 0x1fbfdf22:
+		/* -%C10-%C11-%O$tDCC RECV: Cannot open $1 for writing - aborting. */
+				text = strdup (te[XP_TE_DCCFILEERR].def);
+				break;
+
+			default:
+				text = strdup (ofs);
+			}
+#else
+			text = strdup (ofs);
+#endif
+
+			continue;
+		}/* else if (strcmp (buf, "event_sound") == 0)
+		{
+			if (snd)
+				free (snd);
+			snd = strdup (ofs);
+			continue;
+		}*/
+
+		continue;
+	}
+
+	pevent_trigger_load (&penum, &text, &snd);
+	free (ibuf);
+	return 0;
+}
+
+static void
+pevent_check_all_loaded ()
+{
+	int i;
+
+	for (i = 0; i < NUM_XP; i++)
+	{
+		if (pntevts_text[i] == NULL)
+		{
+			/*printf ("%s\n", te[i].name);
+			snprintf(out, sizeof(out), "The data for event %s failed to load. Reverting to defaults.\nThis may be because a new version of XChat is loading an old config file.\n\nCheck all print event texts are correct", evtnames[i]);
+			   gtkutil_simpledialog(out); */
+			/* make-te.c sets this 128 flag (DON'T call gettext() flag) */
+			if (te[i].num_args & 128)
+				pntevts_text[i] = strdup (te[i].def);
+			else
+				pntevts_text[i] = strdup (_(te[i].def));
+		}
+	}
+}
+
+void
+load_text_events ()
+{
+	memset (&pntevts_text, 0, sizeof (char *) * (NUM_XP));
+	memset (&pntevts, 0, sizeof (char *) * (NUM_XP));
+
+	if (pevent_load (NULL))
+		pevent_load_defaults ();
+	pevent_check_all_loaded ();
+	pevent_make_pntevts ();
+}
+
+/*
+	CL: format_event now handles filtering of arguments:
+	1) if prefs.stripcolor is set, filter all style control codes from arguments
+	2) always strip \010 (ATTR_HIDDEN) from arguments: it is only for use in the format string itself
+*/
+#define ARG_FLAG(argn) (1 << (argn))
+
+void
+format_event (session *sess, int index, char **args, char *o, int sizeofo, unsigned int stripcolor_args)
+{
+	int len, oi, ii, numargs;
+	char *i, *ar, d, a, done_all = FALSE;
+
+	i = pntevts[index];
+	numargs = te[index].num_args & 0x7f;
+
+	oi = ii = len = d = a = 0;
+	o[0] = 0;
+
+	if (i == NULL)
+		return;
+
+	while (done_all == FALSE)
+	{
+		d = i[ii++];
+		switch (d)
+		{
+		case 0:
+			memcpy (&len, &(i[ii]), sizeof (int));
+			ii += sizeof (int);
+			if (oi + len > sizeofo)
+			{
+				printf ("Overflow in display_event (%s)\n", i);
+				o[0] = 0;
+				return;
+			}
+			memcpy (&(o[oi]), &(i[ii]), len);
+			oi += len;
+			ii += len;
+			break;
+		case 1:
+			a = i[ii++];
+			if (a > numargs)
+			{
+				fprintf (stderr,
+							"XChat DEBUG: display_event: arg > numargs (%d %d %s)\n",
+							a, numargs, i);
+				break;
+			}
+			ar = args[(int) a + 1];
+			if (ar == NULL)
+			{
+				printf ("arg[%d] is NULL in print event\n", a + 1);
+			} else
+			{
+				if (stripcolor_args & ARG_FLAG(a + 1)) len = strip_color2 (ar, -1, &o[oi], STRIP_ALL);
+				else len = strip_hidden_attribute (ar, &o[oi]);
+				oi += len;
+			}
+			break;
+		case 2:
+			o[oi++] = '\n';
+			o[oi++] = 0;
+			done_all = TRUE;
+			continue;
+		case 3:
+/*			if (sess->type == SESS_DIALOG)
+			{
+				if (prefs.dialog_indent_nicks)
+					o[oi++] = '\t';
+				else
+					o[oi++] = ' ';
+			} else
+			{*/
+				if (prefs.indent_nicks)
+					o[oi++] = '\t';
+				else
+					o[oi++] = ' ';
+			/*}*/
+			break;
+		}
+	}
+	o[oi] = 0;
+	if (*o == '\n')
+		o[0] = 0;
+}
+
+static void
+display_event (session *sess, int event, char **args, unsigned int stripcolor_args)
+{
+	char o[4096];
+	format_event (sess, event, args, o, sizeof (o), stripcolor_args);
+	if (o[0])
+		PrintText (sess, o);
+}
+
+int
+pevt_build_string (const char *input, char **output, int *max_arg)
+{
+	struct pevt_stage1 *s = NULL, *base = NULL, *last = NULL, *next;
+	int clen;
+	char o[4096], d, *obuf, *i;
+	int oi, ii, max = -1, len, x;
+
+	len = strlen (input);
+	i = malloc (len + 1);
+	memcpy (i, input, len + 1);
+	check_special_chars (i, TRUE);
+
+	len = strlen (i);
+
+	clen = oi = ii = 0;
+
+	for (;;)
+	{
+		if (ii == len)
+			break;
+		d = i[ii++];
+		if (d != '$')
+		{
+			o[oi++] = d;
+			continue;
+		}
+		if (i[ii] == '$')
+		{
+			o[oi++] = '$';
+			continue;
+		}
+		if (oi > 0)
+		{
+			s = (struct pevt_stage1 *) malloc (sizeof (struct pevt_stage1));
+			if (base == NULL)
+				base = s;
+			if (last != NULL)
+				last->next = s;
+			last = s;
+			s->next = NULL;
+			s->data = malloc (oi + sizeof (int) + 1);
+			s->len = oi + sizeof (int) + 1;
+			clen += oi + sizeof (int) + 1;
+			s->data[0] = 0;
+			memcpy (&(s->data[1]), &oi, sizeof (int));
+			memcpy (&(s->data[1 + sizeof (int)]), o, oi);
+			oi = 0;
+		}
+		if (ii == len)
+		{
+			fe_message ("String ends with a $", FE_MSG_WARN);
+			return 1;
+		}
+		d = i[ii++];
+		if (d == 'a')
+		{								  /* Hex value */
+			x = 0;
+			if (ii == len)
+				goto a_len_error;
+			d = i[ii++];
+			d -= '0';
+			x = d * 100;
+			if (ii == len)
+				goto a_len_error;
+			d = i[ii++];
+			d -= '0';
+			x += d * 10;
+			if (ii == len)
+				goto a_len_error;
+			d = i[ii++];
+			d -= '0';
+			x += d;
+			if (x > 255)
+				goto a_range_error;
+			o[oi++] = x;
+			continue;
+
+		 a_len_error:
+			fe_message ("String ends in $a", FE_MSG_WARN);
+			return 1;
+		 a_range_error:
+			fe_message ("$a value is greater than 255", FE_MSG_WARN);
+			return 1;
+		}
+		if (d == 't')
+		{
+			/* Tab - if tabnicks is set then write '\t' else ' ' */
+			s = (struct pevt_stage1 *) malloc (sizeof (struct pevt_stage1));
+			if (base == NULL)
+				base = s;
+			if (last != NULL)
+				last->next = s;
+			last = s;
+			s->next = NULL;
+			s->data = malloc (1);
+			s->len = 1;
+			clen += 1;
+			s->data[0] = 3;
+
+			continue;
+		}
+		if (d < '1' || d > '9')
+		{
+			snprintf (o, sizeof (o), "Error, invalid argument $%c\n", d);
+			fe_message (o, FE_MSG_WARN);
+			return 1;
+		}
+		d -= '0';
+		if (max < d)
+			max = d;
+		s = (struct pevt_stage1 *) malloc (sizeof (struct pevt_stage1));
+		if (base == NULL)
+			base = s;
+		if (last != NULL)
+			last->next = s;
+		last = s;
+		s->next = NULL;
+		s->data = malloc (2);
+		s->len = 2;
+		clen += 2;
+		s->data[0] = 1;
+		s->data[1] = d - 1;
+	}
+	if (oi > 0)
+	{
+		s = (struct pevt_stage1 *) malloc (sizeof (struct pevt_stage1));
+		if (base == NULL)
+			base = s;
+		if (last != NULL)
+			last->next = s;
+		last = s;
+		s->next = NULL;
+		s->data = malloc (oi + sizeof (int) + 1);
+		s->len = oi + sizeof (int) + 1;
+		clen += oi + sizeof (int) + 1;
+		s->data[0] = 0;
+		memcpy (&(s->data[1]), &oi, sizeof (int));
+		memcpy (&(s->data[1 + sizeof (int)]), o, oi);
+		oi = 0;
+	}
+	s = (struct pevt_stage1 *) malloc (sizeof (struct pevt_stage1));
+	if (base == NULL)
+		base = s;
+	if (last != NULL)
+		last->next = s;
+	last = s;
+	s->next = NULL;
+	s->data = malloc (1);
+	s->len = 1;
+	clen += 1;
+	s->data[0] = 2;
+
+	oi = 0;
+	s = base;
+	obuf = malloc (clen);
+	while (s)
+	{
+		next = s->next;
+		memcpy (&obuf[oi], s->data, s->len);
+		oi += s->len;
+		free (s->data);
+		free (s);
+		s = next;
+	}
+
+	free (i);
+
+	if (max_arg)
+		*max_arg = max;
+	if (output)
+		*output = obuf;
+
+	return 0;
+}
+
+
+/* black n white(0/1) are bad colors for nicks, and we'll use color 2 for us */
+/* also light/dark gray (14/15) */
+/* 5,7,8 are all shades of yellow which happen to look dman near the same */
+
+static char rcolors[] = { 19, 20, 22, 24, 25, 26, 27, 28, 29 };
+
+static int
+color_of (char *name)
+{
+	int i = 0, sum = 0;
+
+	while (name[i])
+		sum += name[i++];
+	sum %= sizeof (rcolors) / sizeof (char);
+	return rcolors[sum];
+}
+
+
+/* called by EMIT_SIGNAL macro */
+
+void
+text_emit (int index, session *sess, char *a, char *b, char *c, char *d)
+{
+	char *word[PDIWORDS];
+	int i;
+	unsigned int stripcolor_args = (prefs.stripcolor ? 0xFFFFFFFF : 0);
+	char tbuf[NICKLEN + 4];
+
+	if (prefs.colorednicks && (index == XP_TE_CHANACTION || index == XP_TE_CHANMSG))
+	{
+		snprintf (tbuf, sizeof (tbuf), "\003%d%s", color_of (a), a);
+		a = tbuf;
+		stripcolor_args &= ~ARG_FLAG(1);	/* don't strip color from this argument */
+	}
+
+	word[0] = te[index].name;
+	word[1] = (a ? a : "\000");
+	word[2] = (b ? b : "\000");
+	word[3] = (c ? c : "\000");
+	word[4] = (d ? d : "\000");
+	for (i = 5; i < PDIWORDS; i++)
+		word[i] = "\000";
+
+	if (plugin_emit_print (sess, word))
+		return;
+
+	/* If a plugin's callback executes "/close", 'sess' may be invalid */
+	if (!is_session (sess))
+		return;
+
+	switch (index)
+	{
+	case XP_TE_JOIN:
+	case XP_TE_PART:
+	case XP_TE_PARTREASON:
+	case XP_TE_QUIT:
+		/* implement ConfMode / Hide Join and Part Messages */
+		if (chanopt_is_set (prefs.confmode, sess->text_hidejoinpart))
+			return;
+		break;
+
+	/* ===Private message=== */
+	case XP_TE_PRIVMSG:
+	case XP_TE_DPRIVMSG:
+	case XP_TE_PRIVACTION:
+	case XP_TE_DPRIVACTION:
+		if (chanopt_is_set_a (prefs.input_beep_priv, sess->alert_beep))
+			sound_beep (sess);
+		if (chanopt_is_set_a (prefs.input_flash_priv, sess->alert_taskbar))
+			fe_flash_window (sess);
+		/* why is this one different? because of plugin-tray.c's hooks! ugly */
+		if (sess->alert_tray == SET_ON)
+			fe_tray_set_icon (FE_ICON_MESSAGE);
+		break;
+
+	/* ===Highlighted message=== */
+	case XP_TE_HCHANACTION:
+	case XP_TE_HCHANMSG:
+		if (chanopt_is_set_a (prefs.input_beep_hilight, sess->alert_beep))
+			sound_beep (sess);
+		if (chanopt_is_set_a (prefs.input_flash_hilight, sess->alert_taskbar))
+			fe_flash_window (sess);
+		if (sess->alert_tray == SET_ON)
+			fe_tray_set_icon (FE_ICON_MESSAGE);
+		break;
+
+	/* ===Channel message=== */
+	case XP_TE_CHANACTION:
+	case XP_TE_CHANMSG:
+		if (chanopt_is_set_a (prefs.input_beep_chans, sess->alert_beep))
+			sound_beep (sess);
+		if (chanopt_is_set_a (prefs.input_flash_chans, sess->alert_taskbar))
+			fe_flash_window (sess);
+		if (sess->alert_tray == SET_ON)
+			fe_tray_set_icon (FE_ICON_MESSAGE);
+		break;
+	}
+
+	sound_play_event (index);
+	display_event (sess, index, word, stripcolor_args);
+}
+
+char *
+text_find_format_string (char *name)
+{
+	int i = 0;
+
+	i = pevent_find (name, &i);
+	if (i >= 0)
+		return pntevts_text[i];
+
+	return NULL;
+}
+
+int
+text_emit_by_name (char *name, session *sess, char *a, char *b, char *c, char *d)
+{
+	int i = 0;
+
+	i = pevent_find (name, &i);
+	if (i >= 0)
+	{
+		text_emit (i, sess, a, b, c, d);
+		return 1;
+	}
+
+	return 0;
+}
+
+void
+pevent_save (char *fn)
+{
+	int fd, i;
+	char buf[1024];
+
+	if (!fn)
+		fd = xchat_open_file ("pevents.conf", O_CREAT | O_TRUNC | O_WRONLY,
+									 0x180, XOF_DOMODE);
+	else
+		fd = xchat_open_file (fn, O_CREAT | O_TRUNC | O_WRONLY, 0x180,
+									 XOF_FULLPATH | XOF_DOMODE);
+	if (fd == -1)
+	{
+		/*
+		   fe_message ("Error opening config file\n", FALSE); 
+		   If we get here when X-Chat is closing the fe-message causes a nice & hard crash
+		   so we have to use perror which doesn't rely on GTK
+		 */
+
+		perror ("Error opening config file\n");
+		return;
+	}
+
+	for (i = 0; i < NUM_XP; i++)
+	{
+		write (fd, buf, snprintf (buf, sizeof (buf),
+										  "event_name=%s\n", te[i].name));
+		write (fd, buf, snprintf (buf, sizeof (buf),
+										  "event_text=%s\n\n", pntevts_text[i]));
+	}
+
+	close (fd);
+}
+
+/* =========================== */
+/* ========== SOUND ========== */
+/* =========================== */
+
+char *sound_files[NUM_XP];
+
+void
+sound_beep (session *sess)
+{
+	if (sound_files[XP_TE_BEEP] && sound_files[XP_TE_BEEP][0])
+		/* user defined beep _file_ */
+		sound_play_event (XP_TE_BEEP);
+	else
+		/* system beep */
+		fe_beep ();
+}
+
+static char *
+sound_find_command (void)
+{
+	/* some sensible unix players. You're bound to have one of them */
+	static const char * const progs[] = {"aplay", "esdplay", "soxplay", "artsplay", NULL};
+	char *cmd;
+	int i = 0;
+
+	if (prefs.soundcmd[0])
+		return g_strdup (prefs.soundcmd);
+
+	while (progs[i])
+	{
+		cmd = g_find_program_in_path (progs[i]);
+		if (cmd)
+			return cmd;
+		i++;
+	}
+
+	return NULL;
+}
+
+void
+sound_play (const char *file, gboolean quiet)
+{
+	char buf[512];
+	char wavfile[512];
+	char *file_fs;
+	char *cmd;
+
+	/* the pevents GUI editor triggers this after removing a soundfile */
+	if (!file[0])
+		return;
+
+#ifdef WIN32
+	/* check for fullpath, windows style */
+	if (strlen (file) > 3 &&
+		 file[1] == ':' && (file[2] == '\\' || file[2] == '/') )
+	{
+		strncpy (wavfile, file, sizeof (wavfile));
+	} else
+#endif
+	if (file[0] != '/')
+	{
+		snprintf (wavfile, sizeof (wavfile), "%s/%s", prefs.sounddir, file);
+	} else
+	{
+		strncpy (wavfile, file, sizeof (wavfile));
+	}
+	wavfile[sizeof (wavfile) - 1] = 0;	/* ensure termination */
+
+	file_fs = xchat_filename_from_utf8 (wavfile, -1, 0, 0, 0);
+	if (!file_fs)
+		return;
+
+	if (access (file_fs, R_OK) == 0)
+	{
+		cmd = sound_find_command ();
+
+#ifdef WIN32
+		if (cmd == NULL || strcmp (cmd, "esdplay") == 0)
+		{
+			PlaySound (file_fs, NULL, SND_NODEFAULT|SND_FILENAME|SND_ASYNC);
+		} else
+#endif
+		{
+			if (cmd)
+			{
+				if (strchr (file_fs, ' '))
+					snprintf (buf, sizeof (buf), "%s \"%s\"", cmd, file_fs);
+				else
+					snprintf (buf, sizeof (buf), "%s %s", cmd, file_fs);
+				buf[sizeof (buf) - 1] = '\0';
+				xchat_exec (buf);
+			}
+		}
+
+		if (cmd)
+			g_free (cmd);
+
+	} else
+	{
+		if (!quiet)
+		{
+			snprintf (buf, sizeof (buf), _("Cannot read sound file:\n%s"), wavfile);
+			fe_message (buf, FE_MSG_ERROR);
+		}
+	}
+
+	g_free (file_fs);
+}
+
+void
+sound_play_event (int i)
+{
+	if (sound_files[i])
+		sound_play (sound_files[i], FALSE);
+}
+
+static void
+sound_load_event (char *evt, char *file)
+{
+	int i = 0;
+
+	if (file[0] && pevent_find (evt, &i) != -1)
+	{
+		if (sound_files[i])
+			free (sound_files[i]);
+		sound_files[i] = strdup (file);
+	}
+}
+
+void
+sound_load ()
+{
+	int fd;
+	char buf[512];
+	char evt[128];
+
+	memset (&sound_files, 0, sizeof (char *) * (NUM_XP));
+
+	fd = xchat_open_file ("sound.conf", O_RDONLY, 0, 0);
+	if (fd == -1)
+		return;
+
+	evt[0] = 0;
+	while (waitline (fd, buf, sizeof buf, FALSE) != -1)
+	{
+		if (strncmp (buf, "event=", 6) == 0)
+		{
+			safe_strcpy (evt, buf + 6, sizeof (evt));
+		}
+		else if (strncmp (buf, "sound=", 6) == 0)
+		{
+			if (evt[0] != 0)
+			{
+				sound_load_event (evt, buf + 6);
+				evt[0] = 0;
+			}
+		}
+	}
+
+	close (fd);
+}
+
+void
+sound_save ()
+{
+	int fd, i;
+	char buf[512];
+
+	fd = xchat_open_file ("sound.conf", O_CREAT | O_TRUNC | O_WRONLY, 0x180,
+								 XOF_DOMODE);
+	if (fd == -1)
+		return;
+
+	for (i = 0; i < NUM_XP; i++)
+	{
+		if (sound_files[i] && sound_files[i][0])
+		{
+			write (fd, buf, snprintf (buf, sizeof (buf),
+											  "event=%s\n", te[i].name));
+			write (fd, buf, snprintf (buf, sizeof (buf),
+											  "sound=%s\n\n", sound_files[i]));
+		}
+	}
+
+	close (fd);
+}