templates.diff
author Myhailo Danylenko <isbear@ukrpost.net>
Wed, 15 May 2013 13:07:05 +0300
changeset 85 93c3cc0d7891
parent 48 307314cdbb39
permissions -rw-r--r--
[refresh] Add round-high-priority.diff, refresh => API 41-42 * add round-high-priority.diff * refresh patches according to changes in upstream * API version bump - 41-42

# HG changeset patch
# Parent 00b93fdbb1de8e77b6992cf799715c1da3e61746
[work-in-progress] Use templates for statusbars

diff -r 00b93fdbb1de mcabber/CMakeLists.txt
--- a/mcabber/CMakeLists.txt	Thu Aug 09 00:54:37 2012 +0300
+++ b/mcabber/CMakeLists.txt	Tue Aug 28 20:37:28 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 00b93fdbb1de mcabber/mcabber/hooks.c
--- a/mcabber/mcabber/hooks.c	Thu Aug 09 00:54:37 2012 +0300
+++ b/mcabber/mcabber/hooks.c	Tue Aug 28 20:37:28 2012 +0300
@@ -612,6 +612,10 @@
     ns[0] = imstatus2char[new_status];
 
     hk_run_handlers(HOOK_MY_STATUS_CHANGE, args);
+
+    // FIXME not when modules_enable, but when templates_enable
+    settings_set(SETTINGS_TYPE_OPTION, "my_status", ns);
+    settings_tmpl_set("my_message", msg);
   }
 #endif
 
diff -r 00b93fdbb1de mcabber/mcabber/parser.c
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mcabber/mcabber/parser.c	Tue Aug 28 20:37:28 2012 +0300
@@ -0,0 +1,1116 @@
+
+/* 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"
+
+//
+//  Common
+//
+
+// XXX one for all 'mcabber' quark? to utils?
+GQuark tmpl_gerror_quark ( void )
+{
+	return g_quark_from_static_string ( "tmpl_gerror_quark" );
+}
+
+//
+//  Globs
+//
+
+// 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 );
+}
+
+//
+//  Simple math expressions with variables
+//
+
+typedef enum {
+	no_op,
+	noempty_op,
+	paren_op,
+	plus_op,
+	minus_op,
+	remainder_op,
+	division_op,
+	multiplication_op,
+} math_operator_t;
+
+typedef enum {
+	in_preargument,
+	in_hexoctzero,
+	in_number,
+	in_variable,
+	in_postparen,
+	in_op,
+} math_parser_state_t;
+
+// - + * / % ( )
+static gssize tmpl_math_expand_internal ( const char *str, gsize length, tmpl_variable_callback_t get_val, gpointer udata, GError **error, math_operator_t bound, gsize *proc_len )
+{
+	g_assert ( error != NULL && *error == NULL );
+
+	gssize             result   = 0;
+	const char *       p        = str;
+	const char * const e        = str + length;
+	math_parser_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 ) {
+					math_operator_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;
+}
+
+//
+//  Shell-like variable substitution
+//
+
+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,
+	in_varif,
+	in_postvarif,
+	in_varelse,
+	nostate,
+} var_parser_state_t;
+
+const static char * const var_parser_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",
+	"in_varif",
+	"in_postvarif",
+	"in_varelse",
+	"wrong state",
+};
+
+typedef enum {
+	no_eol         = 0x00,
+	brace_eol      = 0x01,
+	slash_eol      = 0x02,
+	colon_eol      = 0x04,
+	paren_eol      = 0x08,
+	dblparen_eol   = 0x10,
+	balance_parens = 0x20,
+} var_parser_flags_t;
+
+typedef enum {
+	no_operation,
+	expand_var,
+	value_length,
+} var_operation_t;
+
+// 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?tmpl:tmpl}
+//  * ${var:math} ${var:math:math}
+//  * ${#var} ${!var} ${!var[operation]}
+//  * $(( math ))
+//  * \n \t \e \$ \\ \X
+// Callback will be called to obtain variable values.
+// Variable name should be: /^[[:alnum:]][[:alnum:]_-]*$/
+// For pattern rules see tmpl_glob ().
+// For math rules see tmpl_math_expand ().
+// TODO:
+//  * validate variable name in ${!var} expression
+//  * de-hardcode variable name validation rules (or allow pure-numeric names)
+static gchar *tmpl_var_expand_internal ( const char *template, const char * const e, tmpl_variable_callback_t get_val, gpointer udata, gsize *ret_len, GError **error, var_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;
+	var_parser_state_t  state      = in_string;
+	const char         *a1s        = p;     // string, varname
+	const gchar        *value      = NULL;  // variable value
+	gsize               vlen       = 0;     // variable value length
+	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 if ( *p == '?' ) { // conditional expression
+						state = in_varif;
+						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_var_expand_internal ( p, e, 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_var_expand_internal ( p, e, 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_var_expand_internal ( p, e, 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_var_expand_internal ( p, e, 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_var_expand_internal ( p, e, 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_var_expand_internal ( p, e, 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_varif ) {
+			gsize  elen      = 0;
+			gsize  processed = 0;
+			gchar *expansion = tmpl_var_expand_internal ( p, e, get_val, udata, &elen, error, colon_eol | brace_eol, &processed );
+			if ( *error == NULL && vlen != 0 )
+					g_string_append_len ( result, expansion, elen );
+			g_free ( expansion );
+			p     += processed;
+			state  = in_postvarif;
+		} else if ( state == in_postvarif ) {
+			if ( *p == ':' ) {
+				state = in_varelse;
+				p ++;
+			} else if ( *p == '}' )
+				state = in_varend;
+			else
+				g_assert_not_reached ();
+		} else if ( state == in_varelse ) {
+			gsize  elen      = 0;
+			gsize  processed = 0;
+			gchar *expansion = tmpl_var_expand_internal ( p, e, get_val, udata, &elen, error, brace_eol, &processed );
+			if ( *error == NULL && vlen == 0 )
+					g_string_append_len ( result, expansion, elen );
+			g_free ( expansion );
+			p     += processed;
+			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_var_expand_internal ( p, e, 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'", var_parser_statenames [ state < nostate ? state : nostate ] );
+	}
+
+	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 wrapper
+gchar *tmpl_var_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_var_expand_internal ( template, template + len, get_val, udata, ret_len, &error, no_eol, NULL );
+
+	if ( error != NULL ) {
+		g_propagate_error ( ret_err, error );
+		g_free ( result );
+		return NULL;
+	}
+
+	return result;
+}
+
+//
+//  Simple markup language
+//
+
+/*
+typedef enum {
+	ml_color,
+	ml_fill_right,
+	ml_fill_left,
+	ml_fill_both,
+} markup_link_type_t;
+
+typedef guint markup_color_t;
+
+typedef struct {
+	markup_link_type_t type;
+	markup_color_t     color;
+	const char *       s;
+	gssize             len;
+	gboolean           permanent;
+	guint              width;
+} markup_link_t;
+*/
+
+typedef enum {
+	mp_in_string,
+	mp_in_percent,
+	mp_in_color,
+	mp_nostate,
+} markup_parser_state_t;
+
+const static char * const markup_parser_statenames [ ] = {
+	"in string",
+	"in percent",
+	"in color",
+	"wrong state",
+};
+
+void tmpl_markup_free ( markup_link_t * link )
+{
+	markup_link_t * current = link;
+	while ( current != NULL ) {
+		markup_link_t * tmp = current -> next;
+		if ( link -> permanent )
+			g_free ( link -> s );
+		g_slice_free ( markup_link_t, current );
+		current = tmp;
+	}
+}
+
+markup_link_t * link tmpl_markup_permanentize ( markup_link_t * link )
+{
+	markup_link_t * current;
+	for ( current = link; current -> next != NULL; current = current -> next ) {
+		current -> s         = g_strndup ( current -> s, current -> s - current -> e );
+		current -> permanent = TRUE;
+	}
+	return link;
+}
+
+static tmpl_markup_link_t * tmpl_append_markup_link ( markup_link_t *last, markup_link_type_t type, markup_color_t color, const gchar * s, const gchar * e )
+{
+	if ( type == ml_color ) { // it is not splitter
+		if ( last -> s == NULL || last -> len == 0 ) { // append to empty string -- overwrite color & string
+			last -> s     = s;
+			last -> len   = e - s;
+			last -> color = color;
+			return last;
+		} else if ( last -> color == color ) { // color unchanged
+			if ( s == e ) // appending empty string - do nothing
+				return last;
+			else if ( last -> s + last -> len == s ) { // appending continuation of current string
+				last -> len += e - s;
+				return last;
+			}
+		}
+	}
+
+	tmpl_markup_link_t * link = g_slice_new ( markup_link_t );
+	
+	link -> type      = type;
+	link -> color     = color;
+	link -> s         = s;
+	link -> len       = e - s;
+	link -> permanent = FALSE;
+	link -> next      = last -> next; // allow insertion
+
+	last -> next      = link;
+
+	return link;
+}
+
+static markup_link_t * tmpl_markup_expand_internal ( markup_link_t * chain, const char * template, const char * const e, tmpl_shortcut_callback_t get_shortcut, gpointer udata, GError **error )
+{
+	tmpl_markup_link_t *  last  = chain;
+	markup_parser_state_t state = mp_in_string;
+	const char *          p     = template;
+	const char *          s     = template;
+	markup_color_t        color = 0;
+
+	while ( p < e && *error == NULL ) {
+		if ( state == mp_in_string ) {
+			if ( *p == '%' ) {
+				last  = tmpl_append_markup_link ( last, ml_color, last -> color, s, p - s );
+				state = mp_in_percent;
+			}
+			p ++;
+		} else if ( state == mp_in_percent ) {
+			if ( *p == '%' ) {
+				s     = p;
+				state = mp_in_string
+				p ++;
+			} else if ( isalpha ( *p ) ) {
+				gsize        slen     = 0;
+				const char * shortcut = get_shortcut ( *p, &slen, udata );
+				last  = tmpl_markup_expand_internal ( last, shortcut, shortcut + slen, get_shortcut, udata, error );
+				state = mp_in_string;
+				p ++;
+			} else if ( isdigit ( *p ) ) {
+				color = 0;
+				state = mp_in_color;
+			} else if ( *p == '<' ) {
+				p ++;
+				last  = tmpl_append_markup_link ( last, ml_fill_left, last -> color, NULL, 0 );
+				s     = p;
+				state = mp_in_string;
+			} else if ( *p == '|' ) {
+				p ++;
+				last  = tmpl_append_markup_link ( last, ml_fill_both, last -> color, NULL, 0 );
+				s     = p;
+				state = mp_in_string;
+			} else if ( *p == '>' ) {
+				p ++;
+				last  = tmpl_append_markup_link ( last, ml_fill_right, last -> color, NULL, 0 );
+				s     = p;
+				state = mp_in_string;
+			} else
+				g_set_error ( error, TMPL_GERROR_QUARK, TMPL_EWRONGMARKUP, "Wrong symbol in markup expression: '%c'", *p );
+		} else if ( state == mp_in_color ) {
+			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 ( *p == '8' )
+				d = 8;
+			else if ( *p == '9' )
+				d = 9;
+			if ( d >= 0 ) {
+				color = color * 10 + d;
+				p ++;
+			} else {
+				last  = tmpl_append_markup_link ( last, ml_color, color, NULL, 0 );
+				s     = p;
+				state = mp_in_string;
+			}
+		} else
+			g_assert_not_reached ();
+	}
+
+	if ( error == NULL ) {
+		if ( state == mp_in_string )
+			last = tmpl_append_markup_chunk ( last, ml_color, last -> color, s, e );
+		else if ( state == mp_in_color )
+			last  = tmpl_append_markup_link ( last, ml_color, color, NULL, 0 );
+		else
+			g_set_error ( error, TMPL_GERROR_QUARK, TMPL_EWRONGMARKUP,
+			              "Markup parser ended in a wrong state '%s'",
+						  markup_parser_statenames [ state < mp_nostate ? state : mp_nostate ] );
+	}
+
+	return last;
+}
+
+// public wrapper
+markup_link_t * tmpl_markup_expand ( const char *template, gsize len, tmpl_chunk_callback_t get_chunk, gpointer udata, GError **ret_err )
+{
+	g_return_val_if_fail ( ret_err == NULL || *ret_err == NULL, NULL );
+
+	GError        * error  = NULL;
+	markup_link_t * result = g_slice_new ( tmpl_markup_link_t );
+
+	result -> type      = ml_color;
+	result -> color     = 0;
+	result -> s         = NULL;
+	result -> len       = 0;
+	result -> permanent = FALSE;
+	result -> next      = NULL;
+
+	tmpl_markup_expand_internal ( result, template, template + len, get_chunk, udata, &error );
+
+	if ( error != NULL ) {
+		g_propagate_error ( ret_err, error );
+		tmpl_markup_free ( result );
+		return NULL;
+	}
+
+	return result;
+}
+
+/* vim: se ts=4 sw=4: */
diff -r 00b93fdbb1de mcabber/mcabber/parser.h
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mcabber/mcabber/parser.h	Tue Aug 28 20:37:28 2012 +0300
@@ -0,0 +1,154 @@
+
+#ifndef MCABBER_PARSER_H
+#define MCABBER_PARSER_H 1
+
+/* 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>
+
+//
+// Design notes:
+// 
+// Please, keep this part of code free of direct mcabber internals references,
+// as a sort of in-tree library. This way it is much easier to test and debug.
+//
+
+//
+//  Typedefs
+//
+
+#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 )
+
+#define TMPL_EWRONGMARKUP     ( 0x0A )
+
+typedef const char *(*tmpl_variable_callback_t) ( const gchar *name, gsize len, gpointer udata, gsize *ret_len );
+typedef const char *(*tmpl_shortcut_callback_t) ( const gchar name, gpointer udata, gsize *ret_len );
+
+typedef enum {
+	ml_color,
+	ml_fill_right,
+	ml_fill_left,
+	ml_fill_both,
+} markup_link_type_t;
+
+typedef guint markup_color_t;
+
+typedef struct {
+	markup_link_type_t type;
+	markup_color_t     color;
+	const char *       s;
+	gsize              len;
+	gboolean           permanent;
+	gsize              width;
+} markup_link_t;
+
+//
+//  Common
+//
+
+GQuark tmpl_gerror_quark ( void );
+
+//
+//  Simple glob matching
+//
+
+//  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 );
+
+//
+//  Math expansion
+//
+
+//  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 );
+
+//
+//  Variable expansion
+//
+
+//  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 tmpl_glob ().
+// For math expansion rules see tmpl_math_expand ().
+// Returned value should be g_free'd.
+gchar *tmpl_var_expand ( const char * str, gsize len, tmpl_variable_callback_t get_val, gpointer udata, gsize *ret_len, GError ** error );
+
+//
+//  Simple markup parsing
+//
+
+//  tmpl_markup_link_free ( chain )
+// Frees markup links chain (and associated strings, if permanentized).
+void tmpl_markup_free ( markup_link_t * chain );
+
+//  chain tmpl_markup_permanentize ( chain )
+// Strdups strings, used in markup to make them independent of chunk expansions used.
+markup_link_t * tmpl_markup_permanentize ( markup_link_t * chain );
+
+//  chain tmpl_markup_expand ( template, callback )
+// Parses simple markup language:
+//  * %%       - '%' symbol
+//  * %a - %Z  - insert shortcut with this name (expand it as well)
+//  * %123     - text after this will be marked with color 123
+//  * %> %< %| - text padding/trimming marks
+// Returns a chain of markup_link_t structs.
+// String pointers in links can be NULL.
+// String pointers in links point to parts of template or returned by shortcut callback values,
+// so, take care not to modify/free any of them, while using chain. Alternatively, you can
+// tmpl_markup_permanentize () markup.
+// Width fields are reserved for user, they are not set.
+// User have to free returned markup chain with tmpl_markup_free ().
+markup_link_t * tmpl_markup_expand ( const char * str, gsize len, tmpl_shortcut_callback_t get_chunk, gpointer udata, GError ** error );
+
+#endif
+
diff -r 00b93fdbb1de mcabber/mcabber/settings.c
--- a/mcabber/mcabber/settings.c	Thu Aug 09 00:54:37 2012 +0300
+++ b/mcabber/mcabber/settings.c	Tue Aug 28 20:37:28 2012 +0300
@@ -363,6 +363,32 @@
   return 0;
 }
 
+//  settings_tmpl_set(option, value)
+// Sets option, escaping it for use in templates.
+// Could be more efficient with notifiers instead of guards.
+void settings_tmpl_set(const gchar *key, const gchar *value)
+{
+  if (value && *value) {
+    const gchar *p = strchr(value, '%');
+    if (p) {
+      GString *result = g_string_new(NULL);
+      const gchar *s   = value;
+      while (p) {
+        g_string_append_len(result, s, p - s + 1);
+        g_string_append_c(result, '%');
+        s = ++ p;
+        p = strchr(s, '%');
+      }
+      if (*s)
+        g_string_append(result, s);
+      settings_set(SETTNIGS_TYPE_OPTION, key, result -> str);
+      g_string_free(result, TRUE);
+      return;
+    }
+  }
+  settings_set ( SETTNIGS_TYPE_OPTION, key, value );
+}
+
 //  settings_get_status_msg(status)
 // Return a string with the current status message:
 // - if there is a user-defined message ("message" option),
diff -r 00b93fdbb1de mcabber/mcabber/settings.h
--- a/mcabber/mcabber/settings.h	Thu Aug 09 00:54:37 2012 +0300
+++ b/mcabber/mcabber/settings.h	Tue Aug 28 20:37:28 2012 +0300
@@ -37,6 +37,7 @@
 void    settings_opt_set_raw(const gchar *key, const gchar *value);
 void    settings_set(guint type, const gchar *key, const gchar *value);
 void    settings_del(guint type, const gchar *key);
+void    settings_tmpl_set(const gchar *key, const gchar *value);
 const gchar *settings_get(guint type, const gchar *key);
 int     settings_get_int(guint type, const gchar *key);
 const gchar *settings_get_status_msg(enum imstatus status);
diff -r 00b93fdbb1de mcabber/mcabber/templates.c
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mcabber/mcabber/templates.c	Tue Aug 28 20:37:28 2012 +0300
@@ -0,0 +1,467 @@
+
+/* 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 enum {
+	markup_template,   // fully processed $ and % templates
+	shortcut_template, // no-callback $-only but %-propagating templates
+	var_template,      // $-only templates with callback
+} template_type_t;
+
+typedef union {
+	tmpl_var_callback_t    var;
+	tmpl_markup_callback_t markup;
+} template_callback_t;
+
+typedef struct {
+	gchar *             name;           // variable name
+	template_type_t     type;           // can be only one (for simplicity)
+	template_callback_t callback;       //
+	gpointer            userdata;       // userdata for callback
+	GSList *            guards;         // var-guards
+	gboolean            in_use;         // to mark markup & var templates for drop
+	gboolean            var_changed;    // var-reevaluate flag
+	gboolean            markup_changed; // markup-reevaluate flag
+	gboolean            run_callback;   // rerun callback flag
+	gchar *             var_expansion;  // var-expansion of template
+	GSList *            templates;      // markup templates, that use this shortcut template
+	GSList *            shortcuts;      // shortcut templates in use by this markup template
+} 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 );
+}
+
+// [cb] drops template from shortcut's 'templates' list
+static void tmpl_unshortcut ( gpointer data, gpointer udata )
+{
+	template_t * shortcut = data;
+	template_t * template = udata;
+	shortcut -> templates = g_slist_remove ( shortcut -> 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;
+	// not running unshortcut, as it was done earlier
+	// or it is global destruction and chunk may no longer exist
+	g_slist_free ( template -> shortcuts );
+	g_slist_free ( template -> templates );
+	g_slist_foreach ( template -> guards, tmpl_unguard, template );
+	g_slist_free ( template -> guards );
+	g_free ( template -> name );
+	g_free ( template -> var_expansion );
+	g_slice_free ( template_t, template );
+}
+
+// [cb] sets var-changed flag on template
+static void template_set_var_changed ( gpointer data, gpointer udata )
+{
+	template_t * template   = data;
+	template -> var_changed = TRUE;
+}
+
+// [cb] sets markup-changed flag on template
+static void template_set_markup_changed ( gpointer data, gpointer udata )
+{
+	template_t * template      = data;
+	template -> markup_changed = TRUE;
+}
+
+// install guard (name must be glib-allocated string)
+// XXX we are always using the same callback?
+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;
+}
+
+// [parser cb] create & evaluate shortcut template, add to dependencies, return expansion
+static const char *tmpl_get_shortcut ( const gchar name, gpointer udata, gsize *ret_len )
+{
+	gchar        optname [ 7 ] = "tmpl_X\0";
+	template_t * template    = udata;
+	template_t * shortcut;
+	optname [ 5 ] = name;
+	shortcut      = g_hash_table_lookup ( templates, optname );
+	if ( shortcut != NULL ) {
+		if ( shortcut -> type != shortcut_template ) {
+			scr_log_print ( LPRINT_LOGNORM, "Error: Conflicting type of template for shortcut '%c'.", name );
+			return NULL;
+		}
+		// link as dependency
+		if ( ! g_slist_find ( template -> shortcuts, shortcut ) ) {
+			shortcut -> templates = g_slist_prepend ( shortcut -> templates, template );
+			template -> shortcuts = g_slist_prepend ( template -> shortcuts, shortcut );
+		}
+	} else {
+		// create new
+		shortcut = template_new ( name );
+		if ( shortcut == NULL ) {
+			scr_log_print ( LPRINT_LOGNORM, "Error: Unable to create template for shortcut '%c'.", name );
+			return NULL;
+		}
+		shortcut -> type           = shortcut_template;
+		shortcut -> markup_changed = FALSE;
+		// consumes optname
+		tmpl_install_guard ( g_strndup ( optname, 6 ), shortcut, tmpl_guard );
+		// link as dependency
+		shortcut -> templates = g_slist_prepend ( shortcut -> templates, template );
+		template -> shortcuts = g_slist_prepend ( template -> shortcuts, shortcut );
+		// update expansion
+		if ( shortcut -> var_changed ) {
+			GError *error = NULL;
+			shortcut -> var_changed   = FALSE;
+			shortcut -> var_expansion = tmpl_var_expand ( expression, strlen ( expression ), tmpl_get_var, template, NULL, &error );
+			if ( error != NULL ) {
+				scr_log_print ( LPRINT_LOGNORM, "Error: Expansion error on shortcut '%c': %s", name, error -> message );
+				g_error_free ( error );
+				return NULL;
+			}
+		}
+	}
+	return shortcut -> var_expansion;
+}
+
+// [cb]
+// update expansion of all $-changed templates,
+// reinstall guards,
+// mark dependencies as %-changed,
+// call $-callbacks
+static void evaluate_template1 ( gpointer key, gpointer value, gpointer udata )
+{
+	template_t * template = value;
+
+	if ( template -> changed && template -> in_use ) {
+		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_var_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", template -> name, error -> message );
+				g_error_free ( error );
+			}
+		}
+		// re-install guard on template itself
+		tmpl_install_guard ( g_strdup ( template -> name ), template, tmpl_guard );
+		template -> changed = FALSE;
+		// check, if expansion has changed
+		if ( g_strcmp0 ( expansion, template -> var_expansion ) ||
+			 ( template -> type == var_template && template -> run_callback ) ) {
+			g_free ( template -> var_expansion );
+			template -> var_expansion = expansion;
+			if ( template -> type == var_template ) {
+				// pass result to callback
+				template -> run_callback = FALSE;
+				template -> callback.var ( expansion, template -> userdata );
+			} else if ( template -> type == shortcut_template )
+				// mark %-dependencies as %-changed
+				g_slist_foreach ( template -> templates, tmpl_set_markup_changed, NULL );
+			else if ( template -> type == markup_template )
+				// mark self as %-changed
+				template -> markup_changed = TRUE;
+			else
+				g_assert_not_reached ();
+		} else
+			g_free ( expansion );
+	}
+}
+
+// [cb]
+// drop dependencies for unused root templates,
+// evaluate %-changed templates,
+// rebuild dependencies, call %-callbacks
+static void evaluate_template2 ( gpointer key, gpointer value, gpointer udata )
+{
+	template_t * template = value;
+
+	if ( template -> type == markup_template ) {
+		if ( ! template -> in_use ) {
+			// release chunks
+			g_slist_foreach ( template -> shortcuts, tmpl_unshortcut, template );
+			g_slist_free ( template -> shortcuts );
+			template -> shortcuts = NULL;
+		} else if ( template -> markup_changed || template -> run_callback ) {
+			// release chunks
+			g_slist_foreach ( template -> shortcuts, tmpl_unshortcut, template );
+			g_slist_free ( template -> shortcuts );
+			template -> shortcuts = NULL;
+			// evaluate template
+			GError *        error     = NULL;
+			markup_link_t * expansion = tmpl_markup_expand ( template -> var_expansion, strlen ( template -> var_expansion ), tmpl_get_shortcut, template, NULL, &error );
+			if ( error != NULL ) {
+				scr_log_print ( LPRINT_LOGNORM, "Error: Markup error on template '%s': %s.", template -> name, error -> message );
+				g_error_free ( error );
+			}
+			template -> run_callback = FALSE;
+			template -> callback.markup ( expansion, template -> userdata );
+			if ( expansion != NULL )
+				tmpl_markup_free ( expansion ); // TODO cache / check / ...
+		}
+	}
+}
+
+// [cb] mark deleted templates for removal
+static gboolean tmpl_drop_unused_templates ( gpointer key, gpointer value, gpointer udata )
+{
+	template_t * template = value;
+	if ( template -> type == shortcut_template )
+		// drop chunk templates with no dependent root templates
+		return template -> templates == NULL;
+	else
+		// drop user templates, marked as unused
+		return ! template -> in_use;
+}
+
+// [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;
+}
+
+// [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;
+	// update expansion of var-changed templates, call var-callbacks
+	g_hash_table_foreach ( tmpl_templates, evaluate_template1, NULL );
+	// release shortcuts of unused markup templates, reevaluate markup-changed templates, call markup-callbacks
+	g_hash_table_foreach ( tmpl_templates, evaluate_template2, NULL );
+	// drop removed shortcut- and unused markup- and var- templates
+	g_hash_table_foreach_remove ( tmpl_templates, tmpl_drop_unused_templates, 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 );
+	}
+}
+
+// [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_var_changed, NULL );
+		// schedule changed templates reevaluation
+		tmpl_schedule_rerun ();
+	}
+	return g_strdup ( new_value );
+}
+
+// allocates new template and sets generic values
+static template_t *template_new ( const gchar * name )
+{
+	g_assert ( name != NULL );
+	// check for existing template
+	template_t * template = g_hash_table_lookup ( tmpl_templates, name );
+	if ( template != NULL )
+		return NULL;
+	// create new
+	template = g_slice_new ( template_t );
+	template -> name            = g_strdup ( name );
+	template -> userdata        = udata;
+	template -> in_use          = TRUE;
+	template -> guards          = NULL;
+	template -> var_changed     = TRUE;
+	template -> markup_changed  = TRUE;
+	template -> run_callback    = FALSE; // XXX
+	template -> var_expansion   = NULL;
+	template -> templates       = NULL;
+	template -> shortcuts       = NULL;
+	return template;
+}
+
+// public
+gboolean markup_template_add ( const gchar * name, tmpl_markup_callback_t callback, gpointer udata )
+{
+	g_assert ( callback != NULL );
+	
+	template_t * template = template_new ( name );
+	if ( template == NULL )
+		return FALSE;
+	
+	// fill specific fields
+	template -> type            = markup_template;
+	template -> callback.markup = callback;
+	template -> userdata        = udata;
+	template -> markup_changed  = TRUE;
+	// schedule new template evaluation
+	tmpl_schedule_rerun ();
+	return TRUE;
+}
+
+// public
+gboolean var_template_add ( const gchar * name, tmpl_var_callback_t callback, gpointer udata )
+{
+	g_assert ( callback != NULL );
+
+	template_t * template = template_new ( name );
+	if ( template == NULL )
+		return FALSE;
+	
+	// fill specific fields
+	template -> type            = var_template;
+	template -> callback.var    = callback;
+	template -> userdata        = udata;
+	template -> markup_changed  = FALSE;
+	// schedule new template evaluation
+	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 ); // XXX
+	template -> in_use = in_use;
+	tmpl_schedule_rerun (); // XXX
+}
+
+// public
+void template_rerun_callback ( const gchar * name )
+{
+	g_assert ( name != NULL );
+	template_t * template = g_hash_table_lookup ( tmpl_templates, name );
+	g_assert ( template != NULL && template -> type != shortcut_template ); // XXX
+	template -> run_callback = TRUE;
+	tmpl_schedule_rerun ();
+}
+
+// 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 00b93fdbb1de mcabber/mcabber/templates.h
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mcabber/mcabber/templates.h	Tue Aug 28 20:37:28 2012 +0300
@@ -0,0 +1,54 @@
+
+#ifndef MCABBER_TEMPLATES_H
+#define MCABBER_TEMPLATES_H 1
+
+/* 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 <mcabber/parser.h>
+
+// Type for template callback
+typedef void (*tmpl_var_callback_t) ( const gchar * expansion, gpointer udata );
+typedef void (*tmpl_markup_callback_t) ( const markup_link_t * 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 var_template_add ( const gchar * name, tmpl_var_callback_t callback, gpointer udata );
+gboolean markup_template_add ( const gchar * name, tmpl_markup_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 );
+void template_rerun_callback ( const gchar * name );
+
+// XXX do we need this?
+// void tmpl_schedule_rerun ( void );
+
+// private
+void templates_init ( void );
+void templates_uninit ( void );
+
+#endif
+
+/* vim: se ts=4 sw=4: */