/* 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 #include #include #include #include #include #include #include #include "xchat.h" #include #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 #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; 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; lines = 0; while (waitline (fh, buf, sizeof buf, FALSE) != -1) { 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++; } } 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);*/ } 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 (¤ttime))); 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 (¤ttime))); 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
# HexChat ChangeLog

## 2.9.3 (2012-10-14)

 * fix various URL detection bugs
 * fix default folders for file transfers in portable mode
 * fix Autotools warnings with recent releases
 * add /ADDSERVER command
 * add option to save URLs to disk on-the-fly
 * add option to omit alerts when marked as being away
 * add default icons for channel tree and option to turn them off
 * change certain default colors
 * enhance Non-BMP filtering performance
 * accept license agreement by default on Windows
 * update the network list


## 2.9.2 (2012-10-05)

 * fix compilation on Red Hat and Fedora
 * fix portable to non-portable migrations on Windows
 * fix ban message in HexTray
 * fix icon in Connection Complete dialog
 * fix determining if the log folder path is full or relative
 * fix desktop notification icons on Unix
 * fix URL grabber saving an unlimited number of URLs by default
 * fix URL grabber memory leaks under certain circumstances
 * fix URL grabber trying to export URL lists to system folders by default
 * fix opening URLs without http(s)://
 * add support for regenerating text events during compilation on Windows
 * add support for the theme manager on Unix
 * add Unifont to the default list of alternative fonts
 * add option to retain colors in the topic
 * allow the installer to preserve custom GTK+ theme settings on Windows
 * use the icons subfolder of the config folder for loading custom icons
 * use port 6697 for SSL connections by default
 * install the SASL plugin by default on Windows
 * /lastlog improvements
 * build system cosmetics on Unix
 * open links with just left click by default
 * enable timestamps and include seconds by default
 * make libproxy an optional dependency on Unix
 * update German translation
 * update the network list


## 2.9.1 (2012-07-27)

 * fix installing/loading plugins on Unix
 * fix restoring the HexChat window via shortcuts on Windows
 * fix HexTray icon rendering for certain events
 * fix the Show marker line option in Preferences
 * fix /lastlog regexp support on Windows
 * add support for the Checksum, Do At, FiSHLiM and SASL plugins on Unix
 * add option to retain colors when displaying scrollback
 * add MS Gothic to the default list of alternative fonts
 * rebranding and cleanup
 * eliminate lots of compiler warnings
 * Unix build system fixes and cosmetics
 * make Git ignore Unix-specific intermediate files
 * use better compression for Windows installers
 * switch to GTK+ file dialogs on Windows
 * restructure the Preferences window
 * use the addons subfolder of the config folder for auto-loading plugins/scripts
 * improve the dialog used for opening plugins/scripts
 * remember user limits in channel list between sessions
 * remember last search pattern during sessions
 * update XChat to r1521


## 2.9.0 (2012-07-14)

 * rebranding
 * migrate code to GitHub
 * update XChat to r1515
 * fix x64 Perl interface installation for Perl 5.16
 * improve URL detection with new TLDs and file extensions


## 1508-3 (2012-06-17)

 * add XChat Theme Manager
 * fix problems with Turkish locale


## 1508-2 (2012-06-15)

 * add support for Perl 5.16
 * update Do At plugin
 * fix drawing of chat area bottom
 * avoid false hits when restoring from tray via shortcut
 * migrate from NMAKE to Visual Studio


## 1508 (2012-06-02)

 * remove Real Name from Network List
 * search window improvements
 * restore XChat-WDK from tray via shortcut if X-Tray is used


## 1507 (2012-05-13)

 * update OpenSSL to 1.0.1c
 * FiSHLiM updates


## 1506 (2012-05-04)

 * update OpenSSL to 1.0.1b
 * update German translation


## 1503 (2012-03-16)

 * update OpenSSL to 1.0.1
 * URL grabber updates
 * FiSHLiM updates


## 1500 (2012-02-16)

 * add option for specifying alternative fonts
 * fix crash due to invalid timestamp format
 * X-Tray cosmetics


## 1499-7 (2012-02-08)

 * fix update notifications
 * fix compilation on Linux
 * add IPv6 support to built-in identd


## 1499-6 (2012-01-20)

 * add DNS plugin


## 1499-5 (2012-01-20)

 * built-in fix for client crashes
 * update OpenSSL to 1.0.0g


## 1499-4 (2012-01-18)

 * add Non-BMP plugin to avoid client crashes


## 1499-3 (2012-01-15)

 * rework and extend plugin config API
 * add ADD/DEL/LIST support to X-SASL


## 1499-2 (2012-01-11)

 * add X-SASL plugin


## 1499 (2012-01-09)

 * fix saving FiSHLiM keys
 * update OpenSSL to 1.0.0f


## 1498-4 (2011-12-05)

 * fix updates not overwriting old files
 * display WinSys output in one line for others
 * use Strawberry Perl for building


## 1498-3 (2011-12-02)

 * add plugin config API
 * add Exec plugin
 * add WinSys plugin
 * perform periodic update checks automatically


## 1498-2 (2011-11-25)

 * add FiSHLiM plugin
 * add option to allow only one instance of XChat to run


## 1498 (2011-11-23)

 * separate x86 and x64 installers (uninstall any previous version!)
 * downgrade GTK+ to 2.16
 * re-enable the transparent background option
 * various X-Tray improvements
 * add WMPA plugin
 * add Do At plugin
 * automatically save set variables to disk by default
 * update OpenSSL to 1.0.0e


## 1496-6 (2011-08-09)

 * add option to auto-open new tab upon /msg
 * fix the update checker to use the git repo
 * disable update checker cache 


## 1496-5 (2011-08-07)

 * fix attach/detach keyboard shortcut
 * add multi-language support to the spell checker 


## 1496-4 (2011-07-27)

 * recognize Windows 8 when displaying OS info
 * update OpenSSL certificate list
 * fix X-Tray blinking on unselected events
 * fix X-Tray keyboard shortcut handling
 * cease support for Perl 5.10
 * use Strawberry Perl for 5.12 DLLs 


## 1496-3 (2011-06-16)

 * add option for changing spell checker color 


## 1496-2 (2011-06-05)

 * add support for custom license text 


## 1496 (2011-05-30)

 * display build type in CTPC VERSION reply
 * add support for Perl 5.14 


## 1494 (2011-04-16)

 * update Visual Studio to 2010 SP1
 * update OpenSSL to 1.0.0d
 * ship MySpell dictionaries in a separate installer 


## 1489 (2011-01-26)

 * fix unloading the Winamp plugin
 * enable the Favorite Networks feature
 * add Channel Message event support to X-Tray
 * add mpcInfo plugin 


## 1486 (2011-01-16)

 * fix a possible memory leak in the update checker
 * fix XChat-Text shortcut creation
 * fix XChat version check via the plugin interface
 * add option for limiting the size of files to be checksummed
 * add X-Tray as an install option
 * disable Plugin-Tray context menu completely 


## 1479-2 (2011-01-10)

 * improve command-line argument support
 * add auto-copy options
 *