/*************************************************************************
 *  TinyFugue - programmable mud client
 *  Copyright (C) 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2002, 2003, 2004, 2005 Ken Keys
 *
 *  TinyFugue (aka "tf") is protected under the terms of the GNU
 *  General Public License.  See the file "COPYING" for details.
 ************************************************************************/
static const char RCSid[] = "$Id: attr.c,v 35004.7 2005/04/18 03:15:35 kkeys Exp $";

#include "tfconfig.h"
#include "port.h"
#include "tf.h"
#include "util.h"
#include "pattern.h"	/* for tfio.h */
#include "search.h"
#include "tfio.h"
#include "output.h"
#include "attr.h"
#include "variable.h"
#include "parse.h"	/* valstd() */


const int feature_256colors = (NCOLORS == 256);

/* gagged labels will be elided in the "valid values" list of enum2int() */
conString enum_color[]= {
    STRING_LITERAL("black"),
    STRING_LITERAL("red"),
    STRING_LITERAL("green"),
    STRING_LITERAL("yellow"),
    STRING_LITERAL("blue"),
    STRING_LITERAL("magenta"),
    STRING_LITERAL("cyan"),
    STRING_LITERAL("white"),

    STRING_LITERAL("gray"),
    STRING_LITERAL("brightred"),
    STRING_LITERAL_ATTR("brightgreen", F_GAG),
    STRING_LITERAL_ATTR("brightyellow", F_GAG),
    STRING_LITERAL_ATTR("brightblue", F_GAG),
    STRING_LITERAL_ATTR("brightmagenta", F_GAG),
    STRING_LITERAL_ATTR("brightcyan", F_GAG),
    STRING_LITERAL("brightwhite"),

#if NCOLORS == 256
    STRING_LITERAL("rgb000"),
    STRING_LITERAL_ATTR("rgb001", F_GAG),
    STRING_LITERAL_ATTR("rgb002", F_GAG),
    STRING_LITERAL_ATTR("rgb003", F_GAG),
    STRING_LITERAL_ATTR("rgb004", F_GAG),
    STRING_LITERAL_ATTR("rgb005", F_GAG),
    STRING_LITERAL_ATTR("rgb010", F_GAG),
    STRING_LITERAL_ATTR("rgb011", F_GAG),
    STRING_LITERAL_ATTR("rgb012", F_GAG),
    STRING_LITERAL_ATTR("rgb013", F_GAG),
    STRING_LITERAL_ATTR("rgb014", F_GAG),
    STRING_LITERAL_ATTR("rgb015", F_GAG),
    STRING_LITERAL_ATTR("rgb020", F_GAG),
    STRING_LITERAL_ATTR("rgb021", F_GAG),
    STRING_LITERAL_ATTR("rgb022", F_GAG),
    STRING_LITERAL_ATTR("rgb023", F_GAG),
    STRING_LITERAL_ATTR("rgb024", F_GAG),
    STRING_LITERAL_ATTR("rgb025", F_GAG),
    STRING_LITERAL_ATTR("rgb030", F_GAG),
    STRING_LITERAL_ATTR("rgb031", F_GAG),
    STRING_LITERAL_ATTR("rgb032", F_GAG),
    STRING_LITERAL_ATTR("rgb033", F_GAG),
    STRING_LITERAL_ATTR("rgb034", F_GAG),
    STRING_LITERAL_ATTR("rgb035", F_GAG),
    STRING_LITERAL_ATTR("rgb040", F_GAG),
    STRING_LITERAL_ATTR("rgb041", F_GAG),
    STRING_LITERAL_ATTR("rgb042", F_GAG),
    STRING_LITERAL_ATTR("rgb043", F_GAG),
    STRING_LITERAL_ATTR("rgb044", F_GAG),
    STRING_LITERAL_ATTR("rgb045", F_GAG),
    STRING_LITERAL_ATTR("rgb050", F_GAG),
    STRING_LITERAL_ATTR("rgb051", F_GAG),
    STRING_LITERAL_ATTR("rgb052", F_GAG),
    STRING_LITERAL_ATTR("rgb053", F_GAG),
    STRING_LITERAL_ATTR("rgb054", F_GAG),
    STRING_LITERAL_ATTR("rgb055", F_GAG),
    STRING_LITERAL_ATTR("rgb100", F_GAG),
    STRING_LITERAL_ATTR("rgb101", F_GAG),
    STRING_LITERAL_ATTR("rgb102", F_GAG),
    STRING_LITERAL_ATTR("rgb103", F_GAG),
    STRING_LITERAL_ATTR("rgb104", F_GAG),
    STRING_LITERAL_ATTR("rgb105", F_GAG),
    STRING_LITERAL_ATTR("rgb110", F_GAG),
    STRING_LITERAL_ATTR("rgb111", F_GAG),
    STRING_LITERAL_ATTR("rgb112", F_GAG),
    STRING_LITERAL_ATTR("rgb113", F_GAG),
    STRING_LITERAL_ATTR("rgb114", F_GAG),
    STRING_LITERAL_ATTR("rgb115", F_GAG),
    STRING_LITERAL_ATTR("rgb120", F_GAG),
    STRING_LITERAL_ATTR("rgb121", F_GAG),
    STRING_LITERAL_ATTR("rgb122", F_GAG),
    STRING_LITERAL_ATTR("rgb123", F_GAG),
    STRING_LITERAL_ATTR("rgb124", F_GAG),
    STRING_LITERAL_ATTR("rgb125", F_GAG),
    STRING_LITERAL_ATTR("rgb130", F_GAG),
    STRING_LITERAL_ATTR("rgb131", F_GAG),
    STRING_LITERAL_ATTR("rgb132", F_GAG),
    STRING_LITERAL_ATTR("rgb133", F_GAG),
    STRING_LITERAL_ATTR("rgb134", F_GAG),
    STRING_LITERAL_ATTR("rgb135", F_GAG),
    STRING_LITERAL_ATTR("rgb140", F_GAG),
    STRING_LITERAL_ATTR("rgb141", F_GAG),
    STRING_LITERAL_ATTR("rgb142", F_GAG),
    STRING_LITERAL_ATTR("rgb143", F_GAG),
    STRING_LITERAL_ATTR("rgb144", F_GAG),
    STRING_LITERAL_ATTR("rgb145", F_GAG),
    STRING_LITERAL_ATTR("rgb150", F_GAG),
    STRING_LITERAL_ATTR("rgb151", F_GAG),
    STRING_LITERAL_ATTR("rgb152", F_GAG),
    STRING_LITERAL_ATTR("rgb153", F_GAG),
    STRING_LITERAL_ATTR("rgb154", F_GAG),
    STRING_LITERAL_ATTR("rgb155", F_GAG),
    STRING_LITERAL_ATTR("rgb200", F_GAG),
    STRING_LITERAL_ATTR("rgb201", F_GAG),
    STRING_LITERAL_ATTR("rgb202", F_GAG),
    STRING_LITERAL_ATTR("rgb203", F_GAG),
    STRING_LITERAL_ATTR("rgb204", F_GAG),
    STRING_LITERAL_ATTR("rgb205", F_GAG),
    STRING_LITERAL_ATTR("rgb210", F_GAG),
    STRING_LITERAL_ATTR("rgb211", F_GAG),
    STRING_LITERAL_ATTR("rgb212", F_GAG),
    STRING_LITERAL_ATTR("rgb213", F_GAG),
    STRING_LITERAL_ATTR("rgb214", F_GAG),
    STRING_LITERAL_ATTR("rgb215", F_GAG),
    STRING_LITERAL_ATTR("rgb220", F_GAG),
    STRING_LITERAL_ATTR("rgb221", F_GAG),
    STRING_LITERAL_ATTR("rgb222", F_GAG),
    STRING_LITERAL_ATTR("rgb223", F_GAG),
    STRING_LITERAL_ATTR("rgb224", F_GAG),
    STRING_LITERAL_ATTR("rgb225", F_GAG),
    STRING_LITERAL_ATTR("rgb230", F_GAG),
    STRING_LITERAL_ATTR("rgb231", F_GAG),
    STRING_LITERAL_ATTR("rgb232", F_GAG),
    STRING_LITERAL_ATTR("rgb233", F_GAG),
    STRING_LITERAL_ATTR("rgb234", F_GAG),
    STRING_LITERAL_ATTR("rgb235", F_GAG),
    STRING_LITERAL_ATTR("rgb240", F_GAG),
    STRING_LITERAL_ATTR("rgb241", F_GAG),
    STRING_LITERAL_ATTR("rgb242", F_GAG),
    STRING_LITERAL_ATTR("rgb243", F_GAG),
    STRING_LITERAL_ATTR("rgb244", F_GAG),
    STRING_LITERAL_ATTR("rgb245", F_GAG),
    STRING_LITERAL_ATTR("rgb250", F_GAG),
    STRING_LITERAL_ATTR("rgb251", F_GAG),
    STRING_LITERAL_ATTR("rgb252", F_GAG),
    STRING_LITERAL_ATTR("rgb253", F_GAG),
    STRING_LITERAL_ATTR("rgb254", F_GAG),
    STRING_LITERAL_ATTR("rgb255", F_GAG),
    STRING_LITERAL_ATTR("rgb300", F_GAG),
    STRING_LITERAL_ATTR("rgb301", F_GAG),
    STRING_LITERAL_ATTR("rgb302", F_GAG),
    STRING_LITERAL_ATTR("rgb303", F_GAG),
    STRING_LITERAL_ATTR("rgb304", F_GAG),
    STRING_LITERAL_ATTR("rgb305", F_GAG),
    STRING_LITERAL_ATTR("rgb310", F_GAG),
    STRING_LITERAL_ATTR("rgb311", F_GAG),
    STRING_LITERAL_ATTR("rgb312", F_GAG),
    STRING_LITERAL_ATTR("rgb313", F_GAG),
    STRING_LITERAL_ATTR("rgb314", F_GAG),
    STRING_LITERAL_ATTR("rgb315", F_GAG),
    STRING_LITERAL_ATTR("rgb320", F_GAG),
    STRING_LITERAL_ATTR("rgb321", F_GAG),
    STRING_LITERAL_ATTR("rgb322", F_GAG),
    STRING_LITERAL_ATTR("rgb323", F_GAG),
    STRING_LITERAL_ATTR("rgb324", F_GAG),
    STRING_LITERAL_ATTR("rgb325", F_GAG),
    STRING_LITERAL_ATTR("rgb330", F_GAG),
    STRING_LITERAL_ATTR("rgb331", F_GAG),
    STRING_LITERAL_ATTR("rgb332", F_GAG),
    STRING_LITERAL_ATTR("rgb333", F_GAG),
    STRING_LITERAL_ATTR("rgb334", F_GAG),
    STRING_LITERAL_ATTR("rgb335", F_GAG),
    STRING_LITERAL_ATTR("rgb340", F_GAG),
    STRING_LITERAL_ATTR("rgb341", F_GAG),
    STRING_LITERAL_ATTR("rgb342", F_GAG),
    STRING_LITERAL_ATTR("rgb343", F_GAG),
    STRING_LITERAL_ATTR("rgb344", F_GAG),
    STRING_LITERAL_ATTR("rgb345", F_GAG),
    STRING_LITERAL_ATTR("rgb350", F_GAG),
    STRING_LITERAL_ATTR("rgb351", F_GAG),
    STRING_LITERAL_ATTR("rgb352", F_GAG),
    STRING_LITERAL_ATTR("rgb353", F_GAG),
    STRING_LITERAL_ATTR("rgb354", F_GAG),
    STRING_LITERAL_ATTR("rgb355", F_GAG),
    STRING_LITERAL_ATTR("rgb400", F_GAG),
    STRING_LITERAL_ATTR("rgb401", F_GAG),
    STRING_LITERAL_ATTR("rgb402", F_GAG),
    STRING_LITERAL_ATTR("rgb403", F_GAG),
    STRING_LITERAL_ATTR("rgb404", F_GAG),
    STRING_LITERAL_ATTR("rgb405", F_GAG),
    STRING_LITERAL_ATTR("rgb410", F_GAG),
    STRING_LITERAL_ATTR("rgb411", F_GAG),
    STRING_LITERAL_ATTR("rgb412", F_GAG),
    STRING_LITERAL_ATTR("rgb413", F_GAG),
    STRING_LITERAL_ATTR("rgb414", F_GAG),
    STRING_LITERAL_ATTR("rgb415", F_GAG),
    STRING_LITERAL_ATTR("rgb420", F_GAG),
    STRING_LITERAL_ATTR("rgb421", F_GAG),
    STRING_LITERAL_ATTR("rgb422", F_GAG),
    STRING_LITERAL_ATTR("rgb423", F_GAG),
    STRING_LITERAL_ATTR("rgb424", F_GAG),
    STRING_LITERAL_ATTR("rgb425", F_GAG),
    STRING_LITERAL_ATTR("rgb430", F_GAG),
    STRING_LITERAL_ATTR("rgb431", F_GAG),
    STRING_LITERAL_ATTR("rgb432", F_GAG),
    STRING_LITERAL_ATTR("rgb433", F_GAG),
    STRING_LITERAL_ATTR("rgb434", F_GAG),
    STRING_LITERAL_ATTR("rgb435", F_GAG),
    STRING_LITERAL_ATTR("rgb440", F_GAG),
    STRING_LITERAL_ATTR("rgb441", F_GAG),
    STRING_LITERAL_ATTR("rgb442", F_GAG),
    STRING_LITERAL_ATTR("rgb443", F_GAG),
    STRING_LITERAL_ATTR("rgb444", F_GAG),
    STRING_LITERAL_ATTR("rgb445", F_GAG),
    STRING_LITERAL_ATTR("rgb450", F_GAG),
    STRING_LITERAL_ATTR("rgb451", F_GAG),
    STRING_LITERAL_ATTR("rgb452", F_GAG),
    STRING_LITERAL_ATTR("rgb453", F_GAG),
    STRING_LITERAL_ATTR("rgb454", F_GAG),
    STRING_LITERAL_ATTR("rgb455", F_GAG),
    STRING_LITERAL_ATTR("rgb500", F_GAG),
    STRING_LITERAL_ATTR("rgb501", F_GAG),
    STRING_LITERAL_ATTR("rgb502", F_GAG),
    STRING_LITERAL_ATTR("rgb503", F_GAG),
    STRING_LITERAL_ATTR("rgb504", F_GAG),
    STRING_LITERAL_ATTR("rgb505", F_GAG),
    STRING_LITERAL_ATTR("rgb510", F_GAG),
    STRING_LITERAL_ATTR("rgb511", F_GAG),
    STRING_LITERAL_ATTR("rgb512", F_GAG),
    STRING_LITERAL_ATTR("rgb513", F_GAG),
    STRING_LITERAL_ATTR("rgb514", F_GAG),
    STRING_LITERAL_ATTR("rgb515", F_GAG),
    STRING_LITERAL_ATTR("rgb520", F_GAG),
    STRING_LITERAL_ATTR("rgb521", F_GAG),
    STRING_LITERAL_ATTR("rgb522", F_GAG),
    STRING_LITERAL_ATTR("rgb523", F_GAG),
    STRING_LITERAL_ATTR("rgb524", F_GAG),
    STRING_LITERAL_ATTR("rgb525", F_GAG),
    STRING_LITERAL_ATTR("rgb530", F_GAG),
    STRING_LITERAL_ATTR("rgb531", F_GAG),
    STRING_LITERAL_ATTR("rgb532", F_GAG),
    STRING_LITERAL_ATTR("rgb533", F_GAG),
    STRING_LITERAL_ATTR("rgb534", F_GAG),
    STRING_LITERAL_ATTR("rgb535", F_GAG),
    STRING_LITERAL_ATTR("rgb540", F_GAG),
    STRING_LITERAL_ATTR("rgb541", F_GAG),
    STRING_LITERAL_ATTR("rgb542", F_GAG),
    STRING_LITERAL_ATTR("rgb543", F_GAG),
    STRING_LITERAL_ATTR("rgb544", F_GAG),
    STRING_LITERAL_ATTR("rgb545", F_GAG),
    STRING_LITERAL_ATTR("rgb550", F_GAG),
    STRING_LITERAL_ATTR("rgb551", F_GAG),
    STRING_LITERAL_ATTR("rgb552", F_GAG),
    STRING_LITERAL_ATTR("rgb553", F_GAG),
    STRING_LITERAL_ATTR("rgb554", F_GAG),
    STRING_LITERAL("rgb555"),

    STRING_LITERAL("gray0"),
    STRING_LITERAL_ATTR("gray1", F_GAG),
    STRING_LITERAL_ATTR("gray2", F_GAG),
    STRING_LITERAL_ATTR("gray3", F_GAG),
    STRING_LITERAL_ATTR("gray4", F_GAG),
    STRING_LITERAL_ATTR("gray5", F_GAG),
    STRING_LITERAL_ATTR("gray6", F_GAG),
    STRING_LITERAL_ATTR("gray7", F_GAG),
    STRING_LITERAL_ATTR("gray8", F_GAG),
    STRING_LITERAL_ATTR("gray9", F_GAG),
    STRING_LITERAL_ATTR("gray10", F_GAG),
    STRING_LITERAL_ATTR("gray11", F_GAG),
    STRING_LITERAL_ATTR("gray12", F_GAG),
    STRING_LITERAL_ATTR("gray13", F_GAG),
    STRING_LITERAL_ATTR("gray14", F_GAG),
    STRING_LITERAL_ATTR("gray15", F_GAG),
    STRING_LITERAL_ATTR("gray16", F_GAG),
    STRING_LITERAL_ATTR("gray17", F_GAG),
    STRING_LITERAL_ATTR("gray18", F_GAG),
    STRING_LITERAL_ATTR("gray19", F_GAG),
    STRING_LITERAL_ATTR("gray20", F_GAG),
    STRING_LITERAL_ATTR("gray21", F_GAG),
    STRING_LITERAL_ATTR("gray22", F_GAG),
    STRING_LITERAL("gray23"),
#endif

    STRING_NULL };


void init_attrs(void)
{
    Var *var;

    for (var = special_var; var->val.name; var++) {
	if (var->func == ch_attr)
	    ch_attr(var);
    }
}

/* Return the result of combining adj into base.  If adj has the 'x' attr,
 * discard base.  If adj has any colors, they override colors in base.  Other
 * attrs are OR'd.
 */
attr_t adj_attr(attr_t base, attr_t adj)
{
    /* XXX BUG:  User should be able to change hiliteattr, do a /recall, and
     * see old hilited lines displayed with the new hiliteattr.  Interpreting
     * F_HILITE here breaks that feature.  But the interpretation is necessary
     * to make colors override correctly when hiliteattr includes a color.
     */
    if (base & F_HILITE) {
	base &= ~F_HILITE;
	base |= hiliteattr;
    }
    if (adj & F_HILITE) {
	adj &= ~F_HILITE;
	adj |= hiliteattr;
    }

    if (adj & F_EXCLUSIVE)
	return adj;
    if (base & adj & F_FGCOLOR)
	base &= ~F_FGCOLORMASK;
    if (base & adj & F_BGCOLOR)
	base &= ~F_BGCOLORMASK;
    return base | adj;
}

/* convert attr string to bitfields */
const char *parse_attrs(const char *str, attr_t *attrp, int delimiter)
{
    int color, len;
    const char *name;
    char buf[16];
    char reject[3] = {',', '\0', '\0'};

    reject[1] = delimiter;
    *attrp = 0;

    if (!str) return "";

    while (*str && *str != delimiter) {
        ++str;
        switch(str[-1]) {
        case ',':  /* skip */             break;
        case 'n':  *attrp |= F_NONE;      break;
        case 'x':  *attrp |= F_EXCLUSIVE; break;
        case 'G':  *attrp |= F_NOHISTORY; break;
        case 'L':  *attrp |= F_NOLOG;     break;
        case 'A':  *attrp |= F_NOACTIVITY;break;
        case 'g':  *attrp |= F_GAG;       break;
        case 'u':  *attrp |= F_UNDERLINE; break;
        case 'r':  *attrp |= F_REVERSE;   break;
        case 'f':  *attrp |= F_FLASH;     break;
        case 'd':  *attrp |= F_DIM;       break;
        case 'B':  *attrp |= F_BOLD;      break;
        case 'b':  *attrp |= F_BELL;      break;
        case 'h':  *attrp |= F_HILITE;    break;
        case 'E':  *attrp |= error_attr;    break;
        case 'W':  *attrp |= warning_attr;  break;
        case 'I':  *attrp |= info_attr;     break;
        case 'C':
	    len = strcspn(str, reject);
            if (str[len] && len < sizeof(buf)) {
                name = strncpy(buf, str, len);
                buf[len] = '\0';
                str += len;
            } else {
                name = str;
                while (*str) ++str;
            }
	    if (strncmp(name, "bg", 2) == 0) {
		if ((color = enum2int(name+2, 0, enum_color, "bgcolor")) < 0)
		    return NULL;
		*attrp = adj_attr(*attrp, bgcolor2attr(color));
	    } else {
		if ((color = enum2int(name, 0, enum_color, "color")) < 0)
		    return NULL;
		*attrp = adj_attr(*attrp, fgcolor2attr(color));
	    }
            break;
        default:
            eprintf("invalid display attribute '%c'", str[-1]);
            return NULL;
        }
    }
    return str;
}

int ch_status_attr(Var *var)
{
    if (!ch_attr(var)) return 0;
    update_status_line(NULL);
    return 1;
}

int ch_attr(Var *var)
{
    Value *val;
    const char *str;
    attr_t attr;

    if (var->val.type & ~(TYPE_STR|TYPE_ATTR)) {
	/* extremely unlikely, so we don't bother to deal with it correctly,
	 * just do enough that we don't crash. */
	internal_error(__FILE__, __LINE__,
	    "ch_attr: variable '%s' has type %04x",
	    var->val.name, var->val.type);
	return 0;
    }
    if (!(val = getvarval(var)) || !(str = valstd(val))) {
        var->val.u.attr = 0;
    } else if (parse_attrs(str, &attr, 0)) {
        var->val.u.attr = attr;
    } else {
	var->val.type &= ~TYPE_ATTR;
        return 0;
    }
    var->val.type |= TYPE_ATTR;
    return 1;
}


static void set_attr(String *line, int offset, attr_t *starting,
    attr_t current)
{
    /* starting_attrs is set by the attrs parameter and/or codes at the
     * beginning of the line.  If no visible mid-line changes occur, there is
     * no need to allocate line->charattrs (which would increase the size of
     * the line by ~5x).  Note that a trailing attribute change is considered
     * a mid-line change; this is sub-optimal, but unprompt() depends on it
     * (it expects prompt->attrs to be the original starting attributes).
     */
    if (!line->charattrs) {
	if (line->len == 0) {
	    /* start of visible line */
	    *starting = current;
	} else if (*starting != current) {
	    /* First mid-line attr change. */
	    check_charattrs(line, line->len, *starting,
		__FILE__, __LINE__);
	}
    }
    if (line->charattrs)
	while (offset < line->len)
	    line->charattrs[offset++] = current;
}

#define ANSI_CSI        (char)0233    /* ANSI terminal Command Sequence Intro */

/* Interpret embedded codes from a subset of ansi codes:
 * ansi attribute/color codes are converted to tf character or line attrs;
 * tabs are expanded (if %expand_tabs is on); all other codes are ignored.
 * (EMUL_DEBUG was handled in handle_socket_input())
 */
String *decode_ansi(const char *s, attr_t attrs, int emul, attr_t *final_attrs)
{
    String *dst;
    int i, colorstate = 0;
    attr_t new = 0;
    attr_t starting_attrs = attrs;

    if (emul == EMUL_RAW || emul == EMUL_DEBUG) {
	if (final_attrs) *final_attrs = attrs;
	return Stringnew(s, -1, attrs);
    }

    dst = Stringnew(NULL, 1, 0);

    for ( ; *s; s++) {
        if ((emul >= EMUL_ANSI_STRIP) &&
            (*s == ANSI_CSI || (s[0] == '\033' && s[1] == '[' && s++)))
        {
            if (!*s) break;            /* in case code got truncated */
            do {
                s++;
                i = strtoint(s, &s);
		if (colorstate < 0) {
		    /* ignoring everything after error */
		} else if (colorstate % 10 == 8) {
		    if (i == 5) colorstate++;
		    else colorstate = -1; /* error */
		} else if (colorstate % 10 == 9) {
		    if (i < 0 || i > 255)
			colorstate = -1; /* error */
		    else {
#if NCOLORS == 256
			if (colorstate / 10 == 3)
			    new = (new & ~F_FGCOLORMASK) | fgcolor2attr(i);
			else
			    new = (new & ~F_BGCOLORMASK) | bgcolor2attr(i);
#endif
			colorstate = 0;
		    }
                } else if (!i || emul < EMUL_ANSI_ATTR) {
                    new = 0;
                } else if (i >= 30 && i <= 37) {
                    new = (new & ~F_FGCOLORMASK) | fgcolor2attr(i - 30);
                } else if (i >= 40 && i <= 47) {
                    new = (new & ~F_BGCOLORMASK) | bgcolor2attr(i - 40);
                } else if (i >= 90 && i <= 97) { /* not really ANSI */
                    new = (new & ~F_FGCOLORMASK) | fgcolor2attr(i - 90 + 8);
                } else if (i >= 100 && i <= 107) { /* not really ANSI */
                    new = (new & ~F_BGCOLORMASK) | bgcolor2attr(i - 100 + 8);
                } else if (i == 38 || i == 48) { /* not really ANSI */
		    colorstate = i;
		    /* Subsequences of the form "38;5;N" or "48;5;N" describe
		     * fg or bg xterm 256-color controls, where 0<=N<=255.
		     * Once we see the start of such a subsequence, any
		     * deviation from the correct form invalidates the rest
		     * of the control sequence.
		     */
                } else switch (i) {
                    case 1:   new |= F_BOLD;          break;
                    case 4:   new |= F_UNDERLINE;     break;
                    case 5:   new |= F_FLASH;         break;
                    case 7:   new |= F_REVERSE;       break;
                    case 21:  new &= ~F_BOLD;         break;
                    case 22:  new &= ~(F_BOLD|F_DIM); break;
                    case 24:  new &= ~F_UNDERLINE;    break;
                    case 25:  new &= ~F_FLASH;        break;
                    case 27:  new &= ~F_REVERSE;      break;
                    default:  /* ignore it */         break;
                }
            } while (s[0] == ';' && s[1]);

            if (!*s) {			/* in case code got truncated */
		break;
	    } else if (*s == '?') {	/* ignore ESC [ ? alnum */
                if (!*++s) break;
            } else if (*s == 'm') {	/* attribute command */
                attrs = new;
            } /* ignore any other CSI command */

        } else if ((emul >= EMUL_ANSI_STRIP) && (*s == '\033')) {
            /* ignore ESC # digit, ESC ( alnum, ESC ) alnum, and ESC alnum. */
            if (!*++s) break;
            if (*s == '(' || *s == ')' || *s == '#')
                if (!*++s) break;

        } else if (is_print(*s) || *s == '\t') {
	    int orig_len = dst->len;
	    if (*s == '\t' && expand_tabs) {
		Stringnadd(dst, ' ', tabsize - dst->len % tabsize);
	    } else {
		Stringadd(dst, *s);
	    }
	    set_attr(dst, orig_len, &starting_attrs, attrs);

        } else if (*s == '\b') {
	    /* bug: doesn't handle expanded tabs */
	    if (dst->len > 0)
		Stringtrunc(dst, dst->len - 1);

        } else if (*s == '\07') {
            dst->attrs |= F_BELL;
        }
    }

    if (!dst->charattrs) {
        /* No mid-line changes, so apply starting_attrs to entire line */
        dst->attrs |= starting_attrs;
    } else {
        dst->charattrs[dst->len] = attrs;
    }

    if (final_attrs) *final_attrs = attrs;
    return dst;
}

/* Convert embedded '@' codes to internal character or line attrs. */
String *decode_attr(const conString *src, attr_t attrs, int offset)
{
    const char *s;
    String *dst;
    int off;
    attr_t new;
    attr_t starting_attrs;
    attr_t orig_attrs;
    const cattr_t *orig_charattrs = src->charattrs;

    dst = Stringnew(NULL, src->len, 0);
    if (!src->data) return dst;
    starting_attrs = dst->attrs = adj_attr(src->attrs, attrs);

    for (s = src->data + offset; *s; s++) {
        if (s[0] == '@' && s[1] == '{') {
            s+=2;
            if ((off = (*s == '~'))) s++;
            s = parse_attrs(s, &new, '}');
            if (!s) goto decode_attr_error;
            if (*s != '}') {
                eprintf("unmatched @{");
                goto decode_attr_error;
            }
            if (new & F_BELL && !off) dst->attrs |= F_BELL;
	    new &= ~F_BELL;
            if (new & F_FGCOLOR) attrs &= ~F_FGCOLORMASK;
            if (new & F_BGCOLOR) attrs &= ~F_BGCOLORMASK;
            if (new & F_NONE) attrs = 0;
            if (off) attrs &= ~new;
            else attrs |= new;

        } else {
	    orig_attrs = orig_charattrs ? orig_charattrs[s - src->data] : 0;
            Stringadd(dst, *s);
            set_attr(dst, dst->len - 1, &starting_attrs, adj_attr(orig_attrs, attrs));
            if (s[0] == '@' && s[1] == '@')
                s++;
        }
    }

    if (!dst->charattrs) {
        /* No mid-line changes, so apply starting_attrs to entire line */
        dst->attrs |= starting_attrs;
    } else {
        dst->charattrs[dst->len] = attrs;
    }
    return dst;

decode_attr_error:
    dst->links++;
    Stringfree(dst);
    return NULL;
}

/* appends string representation of attrs to buffer */
String *attr2str(String *buffer, attr_t attrs)
{
    if (attrs & F_NONE)       Stringadd(buffer, 'n');
    if (attrs & F_EXCLUSIVE)  Stringadd(buffer, 'x');
    if (attrs & F_GAG)        Stringadd(buffer, 'g');
    if (attrs & F_NOHISTORY)  Stringadd(buffer, 'G');
    if (attrs & F_NOLOG)      Stringadd(buffer, 'L');
    if (attrs & F_NOACTIVITY) Stringadd(buffer, 'A');
    if (attrs & F_UNDERLINE)  Stringadd(buffer, 'u');
    if (attrs & F_REVERSE)    Stringadd(buffer, 'r');
    if (attrs & F_FLASH)      Stringadd(buffer, 'f');
    if (attrs & F_DIM)        Stringadd(buffer, 'd');
    if (attrs & F_BOLD)       Stringadd(buffer, 'B');
    if (attrs & F_BELL)       Stringadd(buffer, 'b');
    if (attrs & F_HILITE)     Stringadd(buffer, 'h');
    if (attrs & F_FGCOLOR)
        SStringcat(Stringadd(buffer, 'C'), &enum_color[attr2fgcolor(attrs)]);
    if (attrs & F_BGCOLOR) {
        if (attrs & F_FGCOLOR) Stringadd(buffer, ',');
        SStringcat(Stringcat(buffer, "Cbg"), &enum_color[attr2bgcolor(attrs)]);
    }
    return buffer;
}

String *encode_attr(const conString *str, int offset)
{
    attr_t oldattrs = 0, attrs;
    int i;
    String *new;
    
    new = Stringnew(NULL, str->len+1, 0);
    if (!str->charattrs) {
	if (str->attrs)
	    Stringadd(attr2str(Stringcat(new, "@{"), str->attrs), '}');
	for (i = offset; i < str->len; i++) {
	    Stringadd(new, str->data[i]);
	    if (str->data[i] == '@')
		Stringadd(new, '@');
	}
	if (str->attrs)
	    Stringcat(new, "@{n}");
    } else {
	for (i = offset; i < str->len; i++) {
	    attrs = adj_attr(str->attrs, str->charattrs[i]);
	    if ((attrs ^ oldattrs) & F_HWRITE) {
		if (!(attrs & F_HWRITE)) {
		    /* no attrs */
		    Stringcat(new, "@{n}");
		} else if (((oldattrs & ~attrs) & F_ENCODE) == 0) {
		    /* new attrs can be added to old attrs */
		    attr_t added = attrs & ~(oldattrs & F_SIMPLE);
		    if (((attrs ^ oldattrs) & F_FGCOLORS) == 0) /* fg same? */
			added &= ~F_FGCOLORS; /* skip fg */
		    if (((attrs ^ oldattrs) & F_BGCOLORS) == 0) /* bg same? */
			added &= ~F_BGCOLORS; /* skip bg */
		    Stringadd(attr2str(Stringcat(new, "@{"), added & F_HWRITE),
			'}');
		} else {
		    /* attrs are different */
		    Stringadd(attr2str(Stringcat(new, "@{n"), attrs & F_HWRITE),
			'}');
		}
	    }
	    Stringadd(new, str->data[i]);
	    if (str->data[i] == '@')
		Stringadd(new, '@');
	    oldattrs = attrs;
	}
	if (attrs & F_HWRITE)
	    Stringcat(new, "@{n}");
    }
    return new;
}

static String *attr2ansi(String *str, attr_t attrs)
{
#define semi()	if (str->len > orig_len) Stringadd(str,';')
    int orig_len = str->len;
    int color;

    if (attrs & F_HILITE)    attrs |= hiliteattr;

    if (attrs & F_BOLD)      { semi(); Stringcat(str, "1"); }
    if (attrs & F_UNDERLINE) { semi(); Stringcat(str, "4"); }
    if (attrs & F_FLASH)     { semi(); Stringcat(str, "5"); }
    if (attrs & F_REVERSE)   { semi(); Stringcat(str, "7"); }

    if (attrs & F_FGCOLOR) {
	semi();
	color = attr2fgcolor(attrs);
	if (color < 8)
	    Sappendf(str, "%d", color + 30);
	else if (color < 16) /* not really ANSI */
	    Sappendf(str, "%d", color - 8 + 90);
	else /* not really ANSI */
	    Sappendf(str, "38;5;%d", color);
    }
    if (attrs & F_BGCOLOR) {
	semi();
	color = attr2bgcolor(attrs);
	if (color < 8)
	    Sappendf(str, "%d", color + 40);
	else if (color < 16) /* not really ANSI */
	    Sappendf(str, "%d", color - 8 + 100);
	else /* not really ANSI */
	    Sappendf(str, "48;5;%d", color);
    }
#undef semi
    return str;
}

String *encode_ansi(const conString *str, int offset)
{
    attr_t oldattrs = 0, attrs;
    int i;
    String *new;
    
    new = Stringnew(NULL, str->len+1, 0);
    if (!str->charattrs) {
	if (str->attrs)
	    Stringadd(attr2ansi(Stringcat(new, "\033["), str->attrs), 'm');
	Stringcat(new, str->data);
	if (str->attrs)
	    Stringcat(new, "\033[m");
    } else {
	for (i = offset; i < str->len; i++) {
	    attrs = adj_attr(str->attrs, str->charattrs[i]);
	    if ((attrs ^ oldattrs) & F_HWRITE) {
		if (!(attrs & F_HWRITE)) {
		    /* no attrs */
		    Stringcat(new, "\033[m");
		} else if (((oldattrs & ~attrs) & F_ENCODE) == 0) {
		    /* new attrs can be added to old attrs */
		    attr_t added = attrs & ~(oldattrs & F_SIMPLE);
		    if (((attrs ^ oldattrs) & F_FGCOLORS) == 0) /* fg same? */
			added &= ~F_FGCOLORS; /* skip fg */
		    if (((attrs ^ oldattrs) & F_BGCOLORS) == 0) /* bg same? */
			added &= ~F_BGCOLORS; /* skip bg */
		    Stringadd(attr2ansi(Stringcat(new, "\033["),
			added & F_HWRITE), 'm');
		} else {
		    /* attrs are different */
		    Stringadd(attr2ansi(Stringcat(new, "\033[;"),
			attrs & F_HWRITE), 'm');
		}
	    }
	    Stringadd(new, str->data[i]);
	    oldattrs = attrs;
	}
	if (attrs & F_HWRITE)
	    Stringcat(new, "\033[m");
    }
    return new;
}

