templates
changeset 33 ce47dc7fc6c0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/templates	Fri Jul 20 17:55:10 2012 +0300
@@ -0,0 +1,1346 @@
+# HG changeset patch
+# Parent 456d832740146c5c9a0428c9ca0a1a0c431481b0
+[work-in-progress] Use templates for statusbars
+
+diff -r 456d83274014 mcabber/CMakeLists.txt
+--- a/mcabber/CMakeLists.txt	Fri Jul 20 17:29:53 2012 +0300
++++ b/mcabber/CMakeLists.txt	Fri Jul 20 17:30:20 2012 +0300
+@@ -154,8 +154,8 @@
+ 
+ ## Define targets
+ set ( mcabber_SUBSYSTEMS
+-	caps commands compl events hbuf help histolog hooks
+-	modules nohtml otr pgp roster screen settings utf8 utils
++	caps commands compl events hbuf help histolog hooks modules
++	nohtml otr parser pgp roster screen settings templates utf8 utils
+ 	xmpp xmpp_helper xmpp_iq xmpp_iqrequest xmpp_muc xmpp_s10n )
+ if ( NOT MODULES_ENABLE )
+ 	list ( APPEND mcabber_SUBSYSTEMS extcmd fifo )
+diff -r 456d83274014 mcabber/mcabber/parser.c
+--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
++++ b/mcabber/mcabber/parser.c	Fri Jul 20 17:30:20 2012 +0300
+@@ -0,0 +1,853 @@
++
++/* Copyright 2012 Myhailo Danylenko
++ *
++ * This file is part of mcabber
++ *
++ * mcabber 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, see <http://www.gnu.org/licenses/>. */
++
++#include <glib.h>
++
++#include <ctype.h>   // is*
++#include <stdlib.h>  // strtol
++#include <string.h>  // strchr
++
++#include "parser.h"
++
++#include "config.h"
++
++//
++//  Private types
++//
++
++typedef enum {
++	in_string,
++	in_escape,
++	in_var,
++	in_plainvarstart,
++	in_plainvar,
++	in_prevarname,
++	in_varnamestart,
++	in_varname,
++	in_varchop,
++	in_varstrip,
++	in_varstripchop,
++	in_varslash,
++	in_varmatch,
++	in_varpresubst,
++	in_varsubst,
++	in_varcolon,
++	in_varminus,
++	in_varplus,
++	in_varpos,
++	in_postvarpos,
++	in_varlen,
++	in_varsubstring,
++	in_varend,
++	in_evalstart,
++	in_mathexpand,
++	in_dblparen_eol,
++	nostate,
++} tmpl_parser_state_t;
++
++const static char * const tmpl_statenames [ ] = {
++	"in string",
++	"in escape",
++	"in var",
++	"in plainvarstart",
++	"in plainvar",
++	"in prevarname",
++	"in varnamestart",
++	"in varname",
++	"in varchop",
++	"in varstrip",
++	"in varstripchop",
++	"in varslash",
++	"in varmatch",
++	"in varpresubst",
++	"in varsubst",
++	"in varcolon",
++	"in varminus",
++	"in varplus",
++	"in varpos",
++	"in postvarpos",
++	"in varlen",
++	"in varsubstring",
++	"in varend",
++	"in evalstart",
++	"in mathexpand",
++	"in dblparen eol",
++	"wrong state",
++};
++
++typedef enum {
++	no_eol         =  0x0,
++	brace_eol      =  0x1,
++	slash_eol      =  0x2,
++	colon_eol      =  0x4,
++	paren_eol      =  0x8,
++	dblparen_eol   = 0x10,
++	balance_parens = 0x20,
++} tmpl_parser_flags_t;
++
++typedef enum {
++	no_operation,
++	expand_var,
++	value_length,
++} tmpl_var_operation_t;
++
++typedef enum {
++	no_op,
++	noempty_op,
++	paren_op,
++	plus_op,
++	minus_op,
++	remainder_op,
++	division_op,
++	multiplication_op,
++} tmpl_math_op_t;
++
++typedef enum {
++	in_preargument,
++	in_hexoctzero,
++	in_number,
++	in_variable,
++	in_postparen,
++	in_op,
++} tmpl_math_state_t;
++
++//
++//  Code
++//
++
++GQuark tmpl_gerror_quark ( void )
++{
++	return g_quark_from_static_string ( "tmpl_gerror_quark" );
++}
++
++// match tmpl_glob ( string, pattern, greedy, right-to-left )
++// Performs matching of given pattern against given string,
++// returns first substring in input, that matched. In pattern,
++// there are currently two tokens recognized: '*' and '?'.
++const gchar *tmpl_glob ( const char *str, gsize length, const char *pat, gsize patlen, gboolean rtl, gsize *ret_len )
++{
++	*ret_len = 0;
++	if ( rtl ) { // rtl
++		const char *p           = pat + patlen - 1;
++		const char *s           = str + length - 1;
++		const char *matchend    = NULL;
++		const char *patrollback = p;
++		const char *strrollback = s;
++		while ( p >= pat && s >= str ) {
++			if ( *p == '*' ) {
++				if ( matchend == NULL && patrollback != NULL )
++					matchend = strrollback;
++				patrollback = NULL;
++				strrollback = NULL;
++				p --;
++			} else if ( *p == '?' || *p == *s ) {
++				if ( patrollback == NULL ) {
++					patrollback = p;
++					strrollback = s;
++				}
++				p --;
++				s --;
++			} else if ( patrollback != NULL ) {
++				p =    patrollback;
++				s = -- strrollback;
++			} else {
++				s --;
++			}
++		}
++		// now s, p or both have finished run
++		if ( s >= str && patrollback == NULL ) // s remains and final star
++			s = str - 1; // value at the end of loop
++		if ( p >= pat ) { // p remains
++			while ( *p == '*' && p >= pat ) // ignore empty stars
++				p --;
++			if ( p >= pat ) // symbols remain in pattern, no match
++				return NULL;
++		}
++		if ( matchend == NULL && patrollback != NULL ) // no stars
++			matchend = strrollback;
++		if ( matchend != NULL ) {
++			*ret_len = matchend - s;
++			return s + 1;
++		} else
++			return NULL;
++	} else { // ltr
++		const char * const  pe          = pat + patlen;
++		const char * const  se          = str + length;
++		const char         *p           = pat;
++		const char         *s           = str;
++		const char         *matchstart  = NULL;
++		const char         *patrollback = p;
++		const char         *strrollback = s;
++		while ( p < pe && s < se ) {
++			if ( *p == '*' ) {
++				if ( matchstart == NULL && patrollback != NULL )
++					matchstart = strrollback;
++				patrollback = NULL;
++				strrollback = NULL;
++				p ++;
++			} else if ( *p == '?' || *p == *s ) {
++				if ( patrollback == NULL ) {
++					patrollback = p;
++					strrollback = s;
++				}
++				p ++;
++				s ++;
++			} else if ( patrollback != NULL ) { // start/nomatch-recovery eat
++				p =    patrollback;
++				s = ++ strrollback;
++			} else { // star-eat
++				s ++;
++			}
++		}
++		// now s, p or both have finished run
++		if ( s < se && patrollback == NULL ) // s remains and final star
++				s = se;
++		if ( p < pe ) { // p remains
++			while ( *p == '*' && p < pe ) // ignore empty stars
++				p ++;
++			if ( p < pe ) // symbols remain in pattern, no match
++				return NULL;
++		}
++		if ( matchstart == NULL && patrollback != NULL ) // no stars
++			matchstart = strrollback;
++		if ( matchstart != NULL )
++			*ret_len = s - matchstart;
++		return matchstart;
++	}
++}
++
++// match tmpl_greedy_glob ( string, pattern, right-to-left )
++// The same as above, but greedy.
++const gchar *tmpl_greedy_glob ( const char *str, gsize length, const char *pat, gsize patlen, gboolean rtl, gsize *ret_len )
++{
++	const char * const  pe = pat + patlen;
++	const char         *p  = pat;
++	while ( p < pe ) {
++		if ( *p == '*' ) {
++			gsize       llen = 0;
++			const char *left = tmpl_glob ( str, length, pat, p - pat, FALSE, &llen );
++			if ( left != NULL ) {
++				gsize       rlen  = 0;
++				const char *right = tmpl_glob ( left + llen, str + length - ( left + llen ), p + 1, pe - p - 1, TRUE, &rlen );
++				if ( right != NULL ) {
++					*ret_len = right + rlen - left;
++					return left;
++				}
++			}
++			return NULL;
++		}
++		p ++;
++	}
++	return tmpl_glob ( str, length, pat, patlen, rtl, ret_len );
++}
++
++// - + * / % ( )
++static gssize tmpl_math_expand_internal ( const char *str, gsize length, tmpl_variable_callback_t get_val, gpointer udata, GError **error, tmpl_math_op_t bound, gsize *proc_len )
++{
++	g_assert ( error != NULL && *error == NULL );
++
++	gssize             result   = 0;
++	const char *       p        = str;
++	const char * const e        = str + length;
++	tmpl_math_state_t   state    = in_preargument;
++	const char *       varname  = NULL;  // variable name start
++	gsize              base     = 10;    // number base
++	gboolean           invert   = FALSE; // number change sign
++	gboolean           errstate = bound >= noempty_op; // indicates erroneous state
++
++	// process first argument
++	while ( p < e && *error == NULL ) {
++		if ( state == in_preargument ) { // initial space-skipping state
++			if ( isspace ( *p ) )
++				p ++;
++			else if ( *p == '-' ) {
++				invert = ! invert;
++				errstate = TRUE;
++				p ++;
++			} else if ( *p == '+' ) {
++				errstate = TRUE;
++				p ++;
++			} else if ( *p == '(' ) {
++				gsize processed = 0;
++				p ++;
++				result = tmpl_math_expand_internal ( p, e - p, get_val, udata, error, paren_op, &processed );
++				if ( *error == NULL )
++					state = in_postparen;
++				errstate  = TRUE;
++				p        += processed;
++			} else if ( *p == '0' ) {
++				result   = 0;
++				errstate = FALSE;
++				state    = in_hexoctzero;
++				p ++;
++			} else if ( isdigit ( *p ) ) {
++				result   = 0;
++				base     = 10;
++				state    = in_number;
++			} else if ( isalpha ( *p ) || *p == '_' ) {
++				state    = in_variable;
++				errstate = FALSE;
++				varname  = p;
++				p ++;
++			} else
++				g_set_error ( error, TMPL_GERROR_QUARK, TMPL_EMATHWRONGARG, "Wrong symbol in math argument" );
++		} else if ( state == in_hexoctzero ) {
++			if ( *p == 'x' ) {
++				errstate = TRUE;
++				base     = 16;
++				p ++;
++			} else
++				base = 8;
++			state  = in_number;
++		} else if ( state == in_number ) {
++			short d = -1;
++			if ( *p == '0' )
++				d = 0;
++			else if ( *p == '1' )
++				d = 1;
++			else if ( *p == '2' )
++				d = 2;
++			else if ( *p == '3' )
++				d = 3;
++			else if ( *p == '4' )
++				d = 4;
++			else if ( *p == '5' )
++				d = 5;
++			else if ( *p == '6' )
++				d = 6;
++			else if ( *p == '7' )
++				d = 7;
++			else if ( base <= 8 )
++				;
++			else if ( *p == '8' )
++				d = 8;
++			else if ( *p == '9' )
++				d = 9;
++			else if ( base <= 10 )
++				;
++			else if ( *p == 'a' || *p == 'A' )
++				d = 10;
++			else if ( *p == 'b' || *p == 'B' )
++				d = 11;
++			else if ( *p == 'c' || *p == 'C' )
++				d = 12;
++			else if ( *p == 'd' || *p == 'D' )
++				d = 13;
++			else if ( *p == 'e' || *p == 'E' )
++				d = 14;
++			else if ( *p == 'f' || *p == 'F' )
++				d = 15;
++			if ( d >= 0 ) {
++				errstate = FALSE;
++				result   = result * base + d;
++				p ++;
++			} else if ( ! errstate )
++				state = in_op;
++			else
++				g_set_error ( error, TMPL_GERROR_QUARK, TMPL_EMATHWRONGNUMBER, "Too short math number" );
++		} else if ( state == in_variable ) {
++			if ( ! ( isalnum ( *p ) || *p == '_' ) ) { // XXX vars with '-' have to be expanded earlier
++				gsize        vlen  = 0;
++				const char * value = get_val ( varname, p - varname, udata, &vlen );
++				// TODO control recursion level
++				result = tmpl_math_expand_internal ( value, vlen, get_val, udata, error, no_op, NULL );
++				if ( *error == NULL )
++					state = in_op;
++			} else
++				p ++;
++		} else if ( state == in_postparen ) {
++			if ( *p == ')' ) {
++				errstate = FALSE;
++				p ++;
++				state    = in_op;
++			} else
++				g_assert_not_reached ();
++		} else if ( state == in_op )
++			break;
++		else
++			g_assert_not_reached ();
++	}
++
++	if ( *error == NULL ) {
++		// in_preargument - empty argument, possibly +-+- - 0 / errstate
++		// in_hexoctzero  - 0                             - 0
++		// in_number      - 0, 0x, 01, 0x1, 1             - failure at 0x (errstate), invert
++		// in_variable    - varname                       - get var, invert
++		// in_postparen   - eof                           - failure (errstate)
++		// in_op          - ok                            - invert, continue parsing
++		if ( errstate )
++			g_set_error ( error, TMPL_GERROR_QUARK, TMPL_EMATHWRONGSTATE, "Wrong state at the end of first math argument parsing" );
++		else {
++			if ( state == in_op ) {
++				if ( invert )
++					result = - result;
++			
++				// process operator+second argument pairs
++				while ( p < e && *error == NULL ) {
++					tmpl_math_op_t op = no_op;
++					if ( isspace ( *p ) )
++						p ++;
++					else if ( *p == '+' ) // set op
++						op = plus_op;
++					else if ( *p == '-' )
++						op = minus_op;
++					else if ( *p == '/' )
++						op = division_op;
++					else if ( *p == '*' )
++						op = multiplication_op;
++					else if ( *p == '%' )
++						op = remainder_op;
++					else if ( *p == ')' ) {
++						if ( bound < paren_op )
++							g_set_error ( error, TMPL_GERROR_QUARK, TMPL_EMATHUNBALPAREN, "Unbalanced parens in math expression" );
++						else
++							break;
++					} else
++						g_set_error ( error, TMPL_GERROR_QUARK, TMPL_EMATHWRONGOP, "Wrong math operator '%c'", *p );
++
++					if ( op != no_op ) {
++						// lower priority operator, returning result
++						if ( op < bound )
++							break;
++
++						p ++;
++						gsize  processed = 0;
++						gssize arg       = tmpl_math_expand_internal ( p, e - p, get_val, udata, error, op, &processed );
++						if ( *error == NULL ) {
++							if ( op == plus_op )
++								result += arg;
++							else if ( op == minus_op )
++								result -= arg;
++							else if ( op == division_op )
++								result /= arg;
++							else if ( op == multiplication_op )
++								result *= arg;
++							else if ( op == remainder_op )
++								result %= arg;
++							else
++								g_assert_not_reached ();
++						}
++						p += processed;
++					}
++				}
++			} else if ( state == in_variable ) {
++				gsize        vlen  = 0;
++				const char * value = get_val ( varname, p - varname, udata, &vlen );
++				result = tmpl_math_expand_internal ( value, vlen, get_val, udata, error, no_op, NULL );
++				if ( *error == NULL && invert )
++					result = - result;
++			} else if ( invert )
++				result = - result;
++			// XXX may add long conditional with g_assert here, but I'm lazy
++		}
++	}
++
++	// return result
++	if ( proc_len != NULL )
++		*proc_len = p - str;
++	return result;
++}
++
++// result tmpl_math_expand ( plainexpanded string, callback )
++// Performs mathematical expansion on given string.
++// Note, that it does not perform variable/quote/etc expansion,
++// if you want it, you should do that first.
++gssize tmpl_math_expand ( const char *str, gsize length, tmpl_variable_callback_t get_val, gpointer udata, GError **ret_err )
++{
++	g_return_val_if_fail ( ret_err == NULL || *ret_err == NULL, -1 ); // XXX
++
++	GError * error  = NULL;
++	gssize   result = tmpl_math_expand_internal ( str, length, get_val, udata, &error, no_op, NULL );
++	if ( error != NULL )
++		g_propagate_error ( ret_err, error );
++	return result;
++}
++
++// expansion tmpl_expand ( template, callback )
++// Parse template, substitute shell-like expressions:
++// * $var ${var}
++// * ${var##pat} ${var#pat} ${var%%pat} ${var%pat}
++// * ${var/pat} ${var/pat/repl} ${var//pat/repl} ${var/%pat/repl} ${var/#pat/repl}
++// * ${var:+tmpl} ${var:-tmpl}
++// * ${var:uint} ${var:uint:uint}
++// * ${#var} ${!var} ${!var[operation]}
++// * \n \t \e \$ \\ \X
++// Callback will be called to obtain variable values.
++// Variable name should be: /^[[:alnum:]][[:alnum:]_-]*$/
++// For pattern rules see tmpl_glob ().
++static gchar *tmpl_expand_internal ( const char *template, gsize len, tmpl_variable_callback_t get_val, gpointer udata, gsize *ret_len, GError **error, tmpl_parser_flags_t flags, gsize *proc_len )
++{
++	g_assert ( error != NULL && *error == NULL );
++
++	if ( template == NULL || len == 0 )
++		return NULL;
++
++	GString            *result     = g_string_new ( NULL );
++	const char         *p          = template;
++	const char * const  e          = template + len;
++	tmpl_parser_state_t  state      = in_string;
++	const char         *a1s        = p;     // string, varname
++	const gchar        *value      = NULL;  // variable value
++	gsize               vlen       = 0;     // variable value length
++	tmpl_var_operation_t operation  = no_operation;
++	gchar              *pattern    = NULL;
++	gsize               plen       = 0;
++	gboolean            rtl        = FALSE; // strip/chop
++	gboolean            greedy     = FALSE; // strip/chop
++	gboolean            multiglob  = FALSE; // replace
++	gsize               parencount = 0;
++	gssize              subsoffset = -1;
++	gssize              subslength = -1;
++
++	while ( p < e && *error == NULL ) {
++		if ( state == in_string ) { // nothing special
++			if ( *p == '\\' ) { // escape next char
++				g_string_append_len ( result, a1s, p - a1s );
++				state = in_escape;
++			} else if ( *p == '$' ) { // start variable
++				g_string_append_len ( result, a1s, p - a1s );
++				state = in_var;
++			} else if ( flags & balance_parens && *p == '(' ) {
++				parencount ++;
++			} else if ( *p == ')' ) {
++				if ( flags & balance_parens && parencount > 0 ) {
++					parencount --;
++				} else if ( flags & dblparen_eol ) {
++					g_string_append_len ( result, a1s, p - a1s );
++					state = in_dblparen_eol;
++				} else if ( flags & balance_parens )
++					g_set_error ( error, TMPL_GERROR_QUARK, TMPL_EUNBALPAREN, "Unbalanced parentheses" );
++			} else if ( ( flags & brace_eol && *p == '}' ) ||
++			            ( flags & slash_eol && *p == '/' ) ||
++						( flags & colon_eol && parencount == 0 && *p == ':' ) )
++				break;
++			p ++;
++		} else if ( state == in_escape ) { // escape (on escaped char)
++			if ( *p == 'n' )
++				g_string_append_c ( result, '\n' );
++			else if ( *p == 't' )
++				g_string_append_c ( result, '\t' );
++			else if ( *p == 'e' ) // for some experimentation with colors...
++				g_string_append_c ( result, '\033' );
++			else
++				g_string_append_c ( result, *p );
++			p ++;
++			state = in_string;
++			a1s   = p;
++		} else if ( state == in_var ) { // some variable
++			if ( *p == '{' ) { // enclosed variable
++				state = in_prevarname;
++				p ++;
++			} else if ( *p == '(' ) {
++				state = in_evalstart;
++				p ++;
++			} else // unenclosed variable
++				state = in_plainvarstart;
++		} else if ( state == in_plainvarstart ) {
++			if ( isalpha ( *p ) || *p == '_' ) {
++				state = in_plainvar;
++				a1s   = p;
++				p ++;
++			} else
++				g_set_error ( error, TMPL_GERROR_QUARK, TMPL_EWRONGVARNAME, "Wrong symbol at the start of variable name" );
++		} else if ( state == in_plainvar ) { // unenclosed variable
++			if ( ! ( isalnum ( *p ) || *p == '-' || *p == '_' ) ) {
++				gsize        vlen  = 0;
++				const gchar *value = get_val ( a1s, p - a1s, udata, &vlen );
++				g_string_append_len ( result, value, vlen );
++				state = in_string;
++				a1s   = p;
++			} else
++				p ++;
++		} else if ( state == in_prevarname ) { // allow ! and # at varname start
++			if ( *p == '!' ) {
++				operation = expand_var;
++				p++;
++			} else if ( *p == '#' ) {
++				operation = value_length;
++				p++;
++			} else
++				operation = no_operation;
++			state = in_varnamestart;
++		} else if ( state == in_varnamestart ) {
++			if ( isalpha ( *p ) || *p == '_' ) {
++				state = in_varname;
++				a1s   = p;
++				p ++;
++			} else
++				g_set_error ( error, TMPL_GERROR_QUARK, TMPL_EWRONGVARNAME, "Wrong symbol at start of variable name" );
++		} else if ( state == in_varname ) { // enclosed variable name
++			if ( ! ( isalnum ( *p ) || *p == '-' || *p == '_' ) ) {
++				value = get_val ( a1s, p - a1s, udata, &vlen );
++				if ( operation == value_length ) {
++					if ( *p == '}' ) {
++						g_string_append_printf ( result, "%lu", vlen );
++						state = in_varend;
++					} else
++						g_set_error ( error, TMPL_GERROR_QUARK, TMPL_EWRONGVAREXP, "Wrong variable expansion expression" );
++				} else {
++					if ( operation == expand_var && value != NULL && vlen > 0 )
++						value = get_val ( value, vlen, udata, &vlen );
++					if ( *p == '}' ) { // end of variable
++						g_string_append_len ( result, value, vlen );
++						state = in_varend;
++					} else if ( *p == '#' ) { // strip expression
++						state = in_varstrip;
++						p ++;
++					} else if ( *p == '%' ) { // chop expression
++						state = in_varchop;
++						p ++;
++					} else if ( *p == '/' ) { // replace expression
++						state = in_varslash;
++						p ++;
++					} else if ( *p == ':' ) { // substring expression
++						state = in_varcolon;
++						p ++;
++					} else // wrong symbols
++						g_set_error ( error, TMPL_GERROR_QUARK, TMPL_EWRONGVAREXP, "Wrong variable expansion expression" );
++				}
++			} else
++				p ++;
++		} else if ( state == in_varstrip ) { // one of strip expressions
++			if ( *p == '#' ) {
++				greedy = TRUE;
++				p ++;
++			}
++			rtl   = FALSE;
++			state = in_varstripchop;
++		} else if ( state == in_varchop ) { // one of chop expressions
++			if ( *p == '%' ) {
++				greedy = TRUE;
++				p ++;
++			}
++			rtl   = TRUE;
++			state = in_varstripchop;
++		} else if ( state == in_varstripchop ) { // pattern expressions
++			gsize  elen      = 0;
++			gsize  processed = 0;
++			gchar *expansion = tmpl_expand_internal ( p, e - p, get_val, udata, &elen, error, brace_eol, &processed );
++			if ( *error == NULL ) {
++				gsize       mlen  = 0;
++				const char *match = NULL;
++				if ( greedy )
++					match = tmpl_greedy_glob ( value, vlen, expansion, elen, rtl, &mlen );
++				else
++					match = tmpl_glob ( value, vlen, expansion, elen, rtl, &mlen );
++				if ( mlen > 0 ) {
++					if ( rtl ) { // % and %%
++						if ( match + mlen == value + vlen )
++							vlen -= mlen;
++					} else if ( match == value ) { // # and ##
++						value  = match + mlen;
++						vlen  -= mlen;
++					}
++				}
++				g_string_append_len ( result, value, vlen );
++			}
++			g_free ( expansion );
++			p     += processed;
++			state  = in_varend;
++		} else if ( state == in_varslash ) { // replace expression
++			multiglob = FALSE;
++			rtl       = FALSE;
++			if ( *p == '#' )
++				p ++;
++			else if ( *p == '%' ) {
++				rtl = TRUE;
++				p ++;
++			} else if ( *p == '/' ) {
++				multiglob = TRUE;
++				p ++;
++			}
++			state = in_varmatch;
++		} else if ( state == in_varmatch ) { // match part of replace expression
++			gsize processed  = 0;
++			pattern          = tmpl_expand_internal ( p, e - p, get_val, udata, &plen, error, brace_eol | slash_eol, &processed );
++			p               += processed;
++			state            = in_varpresubst;
++		} else if ( state == in_varpresubst ) { // skip slash
++			if ( *p == '/' )
++				p ++;
++			state = in_varsubst;
++		} else if ( state == in_varsubst ) { // replace expression
++			gsize  processed = 0;
++			gsize  slen      = 0;
++			gchar *subst     = tmpl_expand_internal ( p, e - p, get_val, udata, &slen, error, brace_eol, &processed );
++			if ( *error == NULL ) {
++				const char       * start = value;
++				const char * const ve    = value + vlen;
++				while ( start < ve ) {
++					gsize       mlen  = 0;
++					const char *match = tmpl_glob ( start, ve - start, pattern, plen, rtl, &mlen );
++					if ( mlen > 0 ) {
++						g_string_append_len ( result, start, match - start );
++						g_string_append_len ( result, subst, slen );
++						start = match + mlen;
++					} else
++						break;
++					if ( ! multiglob )
++						break;
++				}
++				g_string_append_len ( result, start, ve - start );
++			}
++			g_free ( subst );
++			g_free ( pattern );
++			pattern  = NULL;
++			p       += processed;
++			state    = in_varend;
++		} else if ( state == in_varcolon ) { // substring or substitution expression
++			if ( *p == '-' ) {
++				state = in_varminus;
++				p ++;
++			} else if ( *p == '+' ) {
++				state = in_varplus;
++				p ++;
++			} else {
++				subsoffset = -1;
++				subslength = -1;
++				state      = in_varpos;
++			}
++		} else if ( state == in_varminus || state == in_varplus ) { // zero and non-zero substitution
++			gsize        elen      = 0;
++			gsize        processed = 0;
++			gchar       *expansion = tmpl_expand_internal ( p, e - p, get_val, udata, &elen, error, brace_eol, &processed );
++			if ( *error == NULL ) {
++				if ( ( vlen == 0 && state == in_varminus ) || ( vlen > 0 && state == in_varplus ) )
++					g_string_append_len ( result, expansion, elen );
++				else if ( vlen > 0 && state == in_varminus )
++					g_string_append_len ( result, value, vlen );
++			}
++			g_free ( expansion );
++			p     += processed;
++			state  = in_varend;
++		} else if ( state == in_varpos ) { // substring expression
++			gsize  elen      = 0;
++			gsize  processed = 0;
++			gchar *expansion = tmpl_expand_internal ( p, e - p, get_val, udata, &elen, error, colon_eol | brace_eol | balance_parens, &processed );
++			if ( *error == NULL ) {
++				subsoffset = tmpl_math_expand_internal ( expansion, elen, get_val, udata, error, no_op, NULL );
++				if ( subsoffset < 0 )
++					subsoffset = 0;
++			}
++			g_free ( expansion );
++			p     += processed;
++			state  = in_postvarpos;
++		} else if ( state == in_postvarpos ) {
++			if ( *p == ':' ) {
++				state = in_varlen;
++				p ++;
++			} else if ( *p == '}' )
++				state = in_varsubstring;
++			else
++				g_assert_not_reached ();
++		} else if ( state == in_varlen ) {
++			gsize  elen      = 0;
++			gsize  processed = 0;
++			gchar *expansion = tmpl_expand_internal ( p, e - p, get_val, udata, &elen, error, brace_eol | balance_parens, &processed );
++			if ( *error == NULL ) {
++				subslength = tmpl_math_expand_internal ( expansion, elen, get_val, udata, error, no_op, NULL );
++				if ( subslength < 0 )
++					subslength = 0;
++			}
++			g_free ( expansion );
++			p     += processed;
++			state  = in_varsubstring;
++		} else if ( state == in_varsubstring ) {
++			g_assert ( subsoffset >= 0 );
++			if ( subslength == -1 )
++				subslength = vlen;
++			if ( subsoffset < vlen ) {
++				if ( subsoffset + subslength > vlen )
++					g_string_append_len ( result, value + subsoffset, vlen - subsoffset );
++				else
++					g_string_append_len ( result, value + subsoffset, subslength );
++			}
++			state = in_varend;
++		} else if ( state == in_varend ) { // end of enclosed variable
++			if ( *p == '}' ) {
++				p ++;
++				state = in_string;
++				a1s   = p;
++			} else
++				g_assert_not_reached (); // this state must be used only on '}' or EOL
++		} else if ( state == in_evalstart ) {
++			if ( *p == '(' ) {
++				p ++;
++				state = in_mathexpand;
++			} else
++				g_set_error ( error, TMPL_GERROR_QUARK, TMPL_EUNBALPAREN, "Command evaluation is not supported" );
++		} else if ( state == in_mathexpand ) {
++			gsize  elen      = 0;
++			gsize  processed = 0;
++			gchar *expansion = tmpl_expand_internal ( p, e - p, get_val, udata, &elen, error, dblparen_eol | balance_parens, &processed );
++			if ( *error == NULL ) {
++				gssize value = tmpl_math_expand_internal ( expansion, elen, get_val, udata, error, no_op, NULL );
++				if ( *error == NULL )
++					g_string_append_printf ( result, "%li", value );
++			}
++			g_free ( expansion );
++			p     += processed;
++			state  = in_string;
++			a1s    = p;
++		} else if ( state == in_dblparen_eol ) {
++			if ( *p == ')' ) {
++				p ++;
++				break;
++			} else
++				g_set_error ( error, TMPL_GERROR_QUARK, TMPL_EUNBALPAREN, "Unbalanced parentheses" );
++		} else
++			g_assert_not_reached ();
++	}
++
++	if ( *error == NULL ) {
++		if ( state == in_plainvar ) { // plain variable at the end of template
++			gsize        vlen  = 0;
++			const gchar *value = get_val ( a1s, p - a1s, udata, &vlen );
++			g_string_append_len ( result, value, vlen );
++		} else if ( state == in_string ) // end of string
++			g_string_append_len ( result, a1s, p - a1s );
++		else if ( state != in_dblparen_eol )
++			g_set_error ( error, TMPL_GERROR_QUARK, TMPL_EWRONGENDSTATE, "Parser stopped in incorrect state '%s'", state < nostate ? tmpl_statenames [ state ] : "wrong state" );
++	}
++
++	if ( pattern != NULL )
++		g_free ( pattern );
++
++	if ( proc_len != NULL )
++		*proc_len = p - template;
++	if ( ret_len != NULL )
++		*ret_len = result -> len;
++	return g_string_free ( result, FALSE );
++}
++
++//
++//  public wrappers
++//
++
++gchar *tmpl_expand ( const char *template, gsize len, tmpl_variable_callback_t get_val, gpointer udata, gsize *ret_len, GError **ret_err )
++{
++	g_return_val_if_fail ( ret_err == NULL || *ret_err == NULL, NULL );
++
++	GError * error  = NULL;
++	gchar *  result = tmpl_expand_internal ( template, len, get_val, udata, ret_len, &error, no_eol, NULL );
++	if ( error != NULL )
++		g_propagate_error ( ret_err, error );
++	return result;
++}
++
++/* vim: se ts=4 sw=4: */
+diff -r 456d83274014 mcabber/mcabber/parser.h
+--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
++++ b/mcabber/mcabber/parser.h	Fri Jul 20 17:30:20 2012 +0300
+@@ -0,0 +1,77 @@
++
++#ifndef MCABBER_PARSER_H
++#define MCABBER_PARSER_H
++
++/* Copyright 2012 Myhailo Danylenko
++ *
++ * This file is part of mcabber
++ *
++ * mcabber 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, see <http://www.gnu.org/licenses/>. */
++
++#include <glib.h>
++
++#define TMPL_GERROR_QUARK tmpl_gerror_quark ()
++
++#define TMPL_EWRONGVAREXP   ( 0x01 )
++#define TMPL_EWRONGVARNAME  ( 0x03 )
++#define TMPL_EUNBALPAREN    ( 0x04 )
++#define TMPL_EWRONGENDSTATE ( 0x02 )
++
++#define TMPL_EMATHWRONGARG    ( 0x05 )
++#define TMPL_EMATHWRONGNUMBER ( 0x06 )
++#define TMPL_EMATHUNBALPAREN  ( 0x07 )
++#define TMPL_EMATHWRONGOP     ( 0x08 )
++#define TMPL_EMATHWRONGSTATE  ( 0x09 )
++
++typedef const char *(*tmpl_variable_callback_t)   ( const gchar *name, gsize len, gpointer udata, gsize *ret_len );
++
++GQuark tmpl_gerror_quark ( void );
++
++//  match tmpl_glob ( string, pattern, right-to-left )
++// Performs matching of given pattern against given string,
++// returns substring, that matched. In pattern, there are currently
++// two tokens recognized: '*' and '?'. Two flags determine, if
++// '*' should be greedy and which end of string must match the
++// pattern (i.e. have anchor).
++const char *tmpl_glob ( const char *str, gsize strlen, const char *pat, gsize patlen, gboolean rtl, gsize *ret_len );
++
++//  match tmpl_greedy_glob ( string, pattern, rigt-to-left )
++// The same, as above, but greedy.
++const char *tmpl_greedy_glob ( const char *str, gsize strlen, const char *pat, gsize patlen, gboolean rtl, gsize *ret_len );
++
++//  result tmpl_math_expand ( string, callback )
++// Performs mathematical expansion of given string.
++// Note, that $var expressions are not recognized, you have to
++// supply already expanded string here.
++// Supported operators:
++// ( ) + - * / %
++gssize tmpl_math_expand ( const char *str, gsize len, tmpl_variable_callback_t get_val, gpointer udata, GError **error );
++
++//  expansion tmpl_expand ( template, callback )
++// Parse template, substitute shell-like expressions:
++// * $var ${var}
++// * ${var##pat} ${var#pat} ${var%%pat} ${var%pat}
++// * ${var/pat} ${var/pat/repl} ${var//pat/repl} ${var/%pat/repl} ${var/#pat/repl}
++// * ${var:+tmpl} ${var:-tmpl}
++// * ${var:mathexp} ${var:mathexp:mathexp}
++// * ${#var} ${!var} ${!var[operation]}
++// * $(( mathexp ))
++// * \n \t \e \$ \\ \X
++// Callback will be called to obtain variable values.
++// Variable name should be: /^[[:alnum:]][[:alnum:]_-]*$/
++// For pattern rules see mms_glob ().
++gchar *tmpl_expand ( const char *template, gsize len, tmpl_variable_callback_t get_val, gpointer udata, gsize *ret_len, GError **error );
++
++# endif
++
+diff -r 456d83274014 mcabber/mcabber/templates.c
+--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
++++ b/mcabber/mcabber/templates.c	Fri Jul 20 17:30:20 2012 +0300
+@@ -0,0 +1,277 @@
++
++/* Copyright 2012 Myhailo Danylenko
++ *
++ * This file is part of mcabber
++ *
++ * mcabber 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, see <http://www.gnu.org/licenses/>. */
++
++#include <glib.h>
++
++#include <string.h>    // strlen
++
++#include "logprint.h"
++#include "settings.h"
++#include "main.h"      // main_context
++#include "templates.h"
++#include "parser.h"
++
++//
++//  globals
++//
++
++typedef void (*tmpl_callback_t) ( const gchar * expansion, gpointer udata );
++
++typedef struct {
++	gchar           * name;
++	tmpl_callback_t   callback;
++	gpointer          userdata;
++	gboolean          in_use;
++	GSList          * guards;
++	gboolean          changed;
++	gchar           * prev_expansion;
++} template_t;
++
++typedef struct {
++	gchar  * name;
++	GSList * templates;
++} tmpl_guard_t;
++
++static GHashTable * tmpl_templates   = NULL;
++static GHashTable * tmpl_guards      = NULL;
++static guint        tmpl_attached_id = 0;
++
++//
++//  predeclarations
++//
++
++static gchar *tmpl_guard ( const gchar *key, const gchar *new_value );
++
++//
++//  code
++//
++
++// [cb] drops template from guard's 'templates' list
++static void tmpl_unguard ( gpointer data, gpointer udata )
++{
++	tmpl_guard_t *guard    = data;
++	template_t   *template = udata;
++	guard -> templates = g_slist_remove ( guard -> templates, template );
++}
++
++// [destructor cb] releases guard hash table entry
++static void tmpl_free_guard ( gpointer data )
++{
++	tmpl_guard_t *guard = data;
++	settings_del_guard ( guard -> name );
++	g_slist_free ( guard -> templates );
++	g_free ( guard -> name );
++	g_slice_free ( tmpl_guard_t, guard );
++}
++
++// [destructor cb] releases taken guards and frees command
++static void tmpl_free_template ( gpointer data )
++{
++	template_t *template = data;
++	g_slist_foreach ( template -> guards, tmpl_unguard, template );
++	g_slist_free ( template -> guards );
++	g_free ( template -> name );
++	g_free ( template -> prev_expansion );
++	g_slice_free ( template_t, template );
++}
++
++// install guard (name must be glib-allocated string)
++static void tmpl_install_guard ( gchar *name, template_t *template, settings_guard_t callback )
++{
++	tmpl_guard_t *guard = g_hash_table_lookup ( tmpl_guards, name );
++	if ( guard == NULL ) {
++		if ( ! settings_set_guard ( name, callback ) ) {
++			scr_log_print ( LPRINT_LOGNORM, "Error: Unable to install guard for variable '%s' for template '%s'.", name, template -> name );
++			g_free ( name );
++		} else {
++			guard = g_slice_new ( tmpl_guard_t );
++			guard -> name      = name;
++			guard -> templates = NULL;
++			g_hash_table_replace ( tmpl_guards, guard -> name, guard ); // to be sure
++		}
++	} else
++		g_free ( name );
++	if ( ! g_slist_find ( template -> guards, guard ) ) {
++		template -> guards = g_slist_prepend ( template -> guards, guard );
++		guard -> templates = g_slist_prepend ( guard -> templates, template );
++	}
++}
++
++// [parser cb] provides mcabber option values & reinstalls guards
++static const char *tmpl_get_var ( const gchar *name, gsize len, gpointer udata, gsize *ret_len )
++{
++	const char *result = NULL;
++	if ( name != NULL && len > 0 ) {
++		template_t * template = udata;
++		gchar      * var      = g_strndup ( name, len );
++		result = settings_opt_get ( var );
++		// consumes var
++		tmpl_install_guard ( var, template, tmpl_guard );
++	}
++	if ( ret_len != NULL ) {
++		if ( result != NULL )
++			*ret_len = strlen ( result );
++		else
++			*ret_len = 0;
++	}
++	return result;
++}
++
++// [cb] mark unused guards for removal
++static gboolean tmpl_drop_unused_guards ( gpointer key, gpointer value, gpointer udata )
++{
++	tmpl_guard_t * guard = value;
++	if ( guard -> templates == NULL )
++		return TRUE;
++	return FALSE;
++}
++
++// [cb] mark deleted templates for removal, reevaluate changed
++static gboolean reevaluate_template ( gpointer key, gpointer value, gpointer udata )
++{
++	template_t * template = value;
++
++	if ( ! template -> in_use )
++		return TRUE;
++
++	if ( template -> changed ) {
++		const gchar * expression = settings_opt_get ( template -> name );
++		gchar       * expansion  = NULL;
++		// release guards (but do not free them)
++		g_slist_foreach ( template -> guards, tmpl_unguard, template );
++		g_slist_free ( template -> guards );
++		template -> guards = NULL;
++		// re-install guards & get updated expansion
++		if ( expression != NULL ) {
++			GError *error = NULL;
++			expansion = tmpl_expand ( expression, strlen ( expression ), tmpl_get_var, template, NULL, &error );
++			if ( error != NULL ) {
++				scr_log_print ( LPRINT_LOGNORM, "Error: Expansion error on template '%s': %s.\nExpansion stopped at: '%s'", template -> name, error -> message, expansion );
++				g_error_free ( error );
++				g_free ( expansion );
++				expansion = NULL;
++			}
++		}
++		// re-install guard on template itself
++		tmpl_install_guard ( g_strdup ( template -> name ), template, tmpl_guard );
++		template -> changed = FALSE;
++		// pass result to callback
++		if ( g_strcmp0 ( expansion, template -> prev_expansion ) ) {
++			g_free ( template -> prev_expansion );
++			template -> prev_expansion = expansion;
++			template -> callback ( expansion, template -> userdata );
++		} else
++			g_free ( expansion );
++	}
++
++	return FALSE;
++}
++
++// [idle cb] update commands/guards & call cbs when necessary
++static gboolean reevaluate_templates ( gpointer data )
++{
++	// allow reschedule in a process of reevaluation
++	tmpl_attached_id = 0;
++	// drop removed & reevaluate changed templates
++	g_hash_table_foreach_remove ( tmpl_templates, reevaluate_template, NULL );
++	// free unused guards TODO do only when needed
++	g_hash_table_foreach_remove ( tmpl_guards, tmpl_drop_unused_guards, NULL );
++	// always return false, this is oneshot idle call
++	return FALSE;
++}
++
++// schedule templates reevaluation
++static void tmpl_schedule_rerun ( void )
++{
++	if ( tmpl_attached_id == 0 ) {
++		GSource * source = g_idle_source_new ();
++		g_source_set_callback ( source, reevaluate_templates, NULL, NULL );
++		tmpl_attached_id = g_source_attach ( source, main_context );
++		g_source_unref ( source );
++	}
++}
++
++// [cb] sets changed flag on template
++static void template_set_changed ( gpointer data, gpointer udata )
++{
++	template_t * template = data;
++	template -> changed   = TRUE;
++}
++
++// [guard] generic guard for variable
++static gchar *tmpl_guard ( const gchar *key, const gchar *new_value )
++{
++	if ( g_strcmp0 ( new_value, settings_opt_get ( key ) ) ) {
++		// mark dependent commands as modified
++		tmpl_guard_t *guard = g_hash_table_lookup ( tmpl_guards, key );
++		g_slist_foreach ( guard -> templates, template_set_changed, NULL );
++		// schedule execution of modified commands
++		tmpl_schedule_rerun ();
++	}
++	return g_strdup ( new_value );
++}
++
++// public
++gboolean template_add ( const gchar * name, tmpl_callback_t callback, gpointer udata )
++{
++	g_assert ( name != NULL && callback != NULL );
++	// check for existing template
++	template_t * template = g_hash_table_lookup ( tmpl_templates, name );
++	if ( template != NULL )
++		return FALSE;
++	// create new
++	template = g_slice_new ( template_t );
++	template -> name           = g_strdup ( name );
++	template -> callback       = callback;
++	template -> userdata       = udata;
++	template -> in_use         = TRUE;
++	template -> guards         = NULL;
++	template -> changed        = TRUE;
++	template -> prev_expansion = NULL;
++	// schedule reevaluation
++	tmpl_schedule_rerun ();
++	return TRUE;
++}
++
++// public
++void template_set_in_use ( const gchar * name, gboolean in_use )
++{
++	g_assert ( name != NULL );
++	template_t * template = g_hash_table_lookup ( tmpl_templates, name );
++	g_assert ( template != NULL );
++	template -> in_use = in_use;
++}
++
++// private
++void templates_init ( void )
++{
++	// the key will be freed by destruction cb
++	tmpl_guards    = g_hash_table_new_full ( g_str_hash, g_str_equal, NULL, tmpl_free_guard );
++	tmpl_templates = g_hash_table_new_full ( g_str_hash, g_str_equal, NULL, tmpl_free_template );
++}
++
++// private
++void templates_uninit ( void )
++{
++	if ( tmpl_attached_id != 0 )
++		g_source_remove ( tmpl_attached_id );
++	g_hash_table_destroy ( tmpl_templates );
++	g_hash_table_destroy ( tmpl_guards );
++}
++
++/* vim: se ts=4 sw=4: */
+diff -r 456d83274014 mcabber/mcabber/templates.h
+--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
++++ b/mcabber/mcabber/templates.h	Fri Jul 20 17:30:20 2012 +0300
+@@ -0,0 +1,44 @@
++
++/* Copyright 2012 Myhailo Danylenko
++ *
++ * This file is part of mcabber
++ *
++ * mcabber 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, see <http://www.gnu.org/licenses/>. */
++
++#include <glib.h>
++
++// Type for template callback
++typedef void (*tmpl_callback_t) ( const gchar * expansion, gpointer udata );
++
++//  success template_add ( option name, cb, cb udata )
++// Adds given mcabber option to list of watched templates.
++// If any option, used in that template (or template itself) will change,
++// callback will be called with new expansion of template.
++gboolean template_add ( const gchar * name, tmpl_callback_t callback, gpointer udata );
++
++//  template_set_in_use ( option name, used flag )
++// Marks template as (un)used.
++// Note: Template will be actually removed only on next evaluation run,
++// though call to this function schedules such run. This way, you can
++// mark a bunch of templates as unused and then mark some of them as used.
++void template_set_in_use ( const gchar * name, gboolean in_use );
++
++// XXX do we need this?
++// void tmpl_schedule_rerun ( void );
++
++// private
++void templates_init ( void );
++void templates_uninit ( void );
++
++/* vim: se ts=4 sw=4: */
+diff -r 456d83274014 mcabber/mcabber/utils.c
+--- a/mcabber/mcabber/utils.c	Fri Jul 20 17:29:53 2012 +0300
++++ b/mcabber/mcabber/utils.c	Fri Jul 20 17:30:20 2012 +0300
+@@ -650,6 +650,42 @@
+   g_free(arglst);
+ }
+ 
++//  parse_list(arg, cb, udata)
++// Calls cb for every element in space/semicolon/comma-separated list.
++// Designed to work in-place, so, no escapes, quoting etc.
++// Terminates parsing if callback returns false.
++void parse_list(const char *arg, parse_list_cb_t cb, void *udata)
++{
++  const char *p, *start;
++  enum {
++    in_space,
++    in_string,
++  } state;
++
++  if (!arg) 
++    return;
++
++  state = in_space;
++  while ( *p ) {
++    if ( *p == ' ' || *p == ';' || *p == ',' ) {
++      if ( state == in_string ) {
++        if ( ! cb ( start, p, udata ) )
++          return;
++        state = in_space;
++      }
++    } else {
++      if ( state == in_space ) {
++        start = p;
++        state = in_string;
++      }
++    }
++    p ++;
++  }
++
++  if ( state == in_string )
++    cb ( start, p, udata );
++}
++
+ //  replace_nl_with_dots(bufstr)
+ // Replace '\n' with "(...)" (or with a NUL if the string is too short)
+ void replace_nl_with_dots(char *bufstr)
+diff -r 456d83274014 mcabber/mcabber/utils.h
+--- a/mcabber/mcabber/utils.h	Fri Jul 20 17:29:53 2012 +0300
++++ b/mcabber/mcabber/utils.h	Fri Jul 20 17:30:20 2012 +0300
+@@ -43,6 +43,11 @@
+ char **split_arg(const char *arg, unsigned int n, int dontstriplast);
+ void free_arg_lst(char **arglst);
+ 
++/* fast in-place string split on space/semicolon/comma
++ * stops processing if callback returns false value */
++typedef int (*parse_list_cb_t)(const char *start, const char *end, void *udata);
++void parse_list(const char *arg, parse_list_cb_t cb, void *udata);
++
+ void replace_nl_with_dots(char *bufstr);
+ char *ut_expand_tabs(const char *text);
+