shithub: fc

ref: ef1c78b3ad619f48c4835f1684cf23393d704b3b
dir: /fc.c/

View raw version
#include <u.h>
#include <libc.h>
#include <draw.h>
#include <event.h>
#include <keyboard.h>
#include <bio.h>
#include <ctype.h>

/* Designed in acme and for acme dev */

#define CONFIG_FILE 						"/tmp/fc.conf"
#define DEFAULT_CTL_FILE 					"/tmp/fc.ctl"
#define DEFAULT_SAVE_PATH 					"/tmp/sheet.spr"
#define DEFAULT_BANNER_HEIGHT 				25
#define DEFAULT_STATUS_HEIGHT 				20
#define DEFAULT_STATUS_MARGIN 				10
#define DEFAULT_BOX_LABEL_OFFSET_Y 			16
#define DEFAULT_BOX_TEXT_MARGIN 			5
#define DEFAULT_FORMULA_INDICATOR_OFFSET 	10
#define DEFAULT_DIALOG_WIDTH 				256
#define DEFAULT_DIALOG_HEIGHT 				48
#define DEFAULT_DIALOG_PADDING 				10
#define DEFAULT_EMOJI_SPEED 				3
#define DEFAULT_EMOJI_FRAME_DELAY 			10
#define DEFAULT_CTL_CHECK_INTERVAL 			200
#define DEFAULT_REDRAW_INTERVAL 			30
#define DEFAULT_MAX_EVAL_DEPTH 				10
#define DEFAULT_MAX_RECALC_PASSES 			10
#define DEFAULT_FORMULA_PRECISION 			2
#define DEFAULT_PARAGRAPH_W 				256
#define DEFAULT_PARAGRAPH_H 				128

/* Constants */
#define MAXBOXES 300
#define BOXWIDTH 128
#define BOXHEIGHT 32
#define MAXFORMULA 256
#define MAXCONTENT 256


enum {
	T_TEXT = 0,
	T_NUMBER,
	T_FORMULA,
	T_LABEL,
	T_PARAGRAPH,
	MAXBOXTYPES,
};

enum {
	TOK_NUM = 0,
	TOK_CELL,
	TOK_RANGE,
	TOK_OP,
	TOK_FUNC,
	TOK_LPAREN,
	TOK_RPAREN,
	TOK_COMMA,
	TOK_STRING,
	TOK_END,

	OP_ADD = '+',
	OP_SUB = '-',
	OP_MUL = '*',
	OP_DIV = '/',
	OP_MOD = '%',
	OP_POW = '^',
	OP_EQ = '=',
	OP_NE = '!',
	OP_LT = '<',
	OP_GT = '>',
	OP_LE = '[',
	OP_GE = ']',

	FN_SUM = 0,
	FN_AVG,
	FN_MIN,
	FN_MAX,
	FN_COUNT,
	FN_ABS,
	FN_SQRT,
	FN_POW,
	FN_ROUND,
	FN_FLOOR,
	FN_CEIL,
	FN_IF,
	FN_AND,
	FN_OR,
	FN_NOT,
	FN_CONCAT,
	FN_LEN,
	FN_UPPER,
	FN_LOWER,
	FN_LOOKUP,
	MAXFUNCS,
};

typedef struct Config Config;
struct Config {
	int banner_height;
	int status_height;
	int status_margin;
	int box_label_offset_y;
	int box_text_margin;
	int formula_indicator_offset;
	int dialog_width;
	int dialog_height;
	int dialog_padding;
	int paragraph_height;
	int paragraph_width;
	int emoji_enabled;
	int emoji_speed;
	int emoji_frame_delay;
	int ctl_check_interval;
	int redraw_interval;
	int gridsnap;
	int gridsize;
	int max_eval_depth;
	int max_recalc_passes;
	int formula_precision;
	int show_formula_indicator;
	char ctl_file[256];
	char default_save_path[256];
	ulong color_bg;
	ulong color_fg;
	ulong color_box_bg;
	ulong color_selected;
	ulong color_editing;
	ulong color_grid;
	ulong color_label;
	ulong color_formula;
	char formula_format[32];
};

typedef struct Box Box;
struct Box {
	Point		pos;
	Rectangle	r;
	char		content[MAXCONTENT];
	char		formula[MAXFORMULA];
	char		label[32];
	int			type;
	double		value;
	int			selected;
	int			dirty;
	int			refs[10];
	int			nrefs;
};

typedef struct Sheet Sheet;
struct Sheet {
	Box		boxes[MAXBOXES];
	int		nboxes;
	int		selected;
	int		editing;
	char	editbuf[MAXCONTENT];
	int		editpos;
	int		editing_label;
	char	labelbuf[32];
	int		labelpos;
	int		entering_filename;
	char	filenamebuf[256];
	int		filenamepos;
	int		save_mode;
	int		current_mode;
	Point	offset;
	int		needredraw;
	int		gridsnap;
	int		gridsize;
	int		emoji_pos;
	int		emoji_frame;
	int		emoji_dir;
	char	*emoji_frames[4];
	int		emoji_enabled;
};

typedef struct BoxType BoxType;
struct BoxType {
	char	*name;
	void	(*parse)(Box*);
	void	(*eval)(Box*);
	void	(*draw)(Box*, Image*);
};

typedef struct Token Token;
struct Token {
	int	type;
	union {
		double	num;
		int	cell;
		struct {
			int start, end;
		} range;
		int		op;
		int		func;
		char	str[MAXCONTENT];
	};
};

typedef struct Function Function;
struct Function {
	char	*name;
	int	minargs;
	int	maxargs;
	double	(*eval)(Token *args, int nargs);
};

typedef struct Eval Eval;
struct Eval {
	Token	tokens[256];
	int	ntokens;
	int	pos;
	Box	*current;
	int	depth;
};

typedef void (*KeyHandler)(int key);
typedef void (*MouseHandler)(Mouse m);

typedef struct InputMode InputMode;
struct InputMode {
	char		*name;
	KeyHandler	handler;
	MouseHandler mouse_handler;
	void		(*draw)(void);
	char		*status;
};

typedef struct Command Command;
struct Command {
	int		key;
	void	(*action)(void);
};

typedef struct EditAction EditAction;
struct EditAction {
	int		key;
	int		(*action)(char *buf, int *pos, int maxlen);
};

typedef struct DrawStep DrawStep;
struct DrawStep {
	void (*draw)(void);
	int  condition;
};

typedef struct CommandHandler CommandHandler;
struct CommandHandler {
	char	*name;
	int		minargs;
	void	(*execute)(char **args, int nargs);
};

typedef enum {
	CFG_INT,
	CFG_STRING,
	CFG_COLOR,
	CFG_BOOL
} ConfigType;

typedef struct ConfigField ConfigField;
struct ConfigField {
	char		*name;
	ConfigType	type;
	void		*ptr;
	int 		maxlen;			
	void		(*callback)(void);
};

Image	*colors[6];
Image	*boxbg;
Image	*boxselected;
Image	*boxediting;
Image	*gridcolor;
Config 	config;
Sheet 	sheet;

void load_spr(char *file);
void save_spr(char *file);
void redraw(void);
void recalc_all(void);
void load_config(char *path);
void save_config(char *path);
void apply_config(void);
void parse_paragraph(Box*);
void parse_text(Box*);
void parse_number(Box*);
void parse_formula(Box*);
void eval_text(Box*);
void eval_number(Box*);
void eval_formula(Box*);
void eval_paragraph(Box*);
void handle_normal_mode(int key);
void handle_cell_edit(int key);
void handle_label_edit(int key);
void handle_filename_input(int key);
void handle_normal_mouse(Mouse m);
void handle_edit_mouse(Mouse m);
void handle_label_mouse(Mouse m);
void handle_filename_mouse(Mouse m);
void draw_box_generic(Box*, Image*);
void draw_box_paragraph(Box*, Image*);
void draw_normal_overlay(void);
void draw_cell_edit_overlay(void);
void draw_label_edit_overlay(void);
void draw_filename_overlay(void);
void draw_background(void);
void draw_grid_lines(void);
void draw_all_boxes(void);
void draw_emoji_banner(void);
void draw_status_line(void);
void cmd_quit(void);
void cmd_save(void);
void cmd_save_as(void);
void cmd_open(void);
void cmd_open_file(void);
void cmd_start_label(void);
void cmd_toggle_grid(void);
void cmd_delete_box(void);
void cmd_cycle_emoji(void);
void cmd_toggle_emoji(void);
void cmd_reload_config(void);
void ctl_addbox(char**, int);
void ctl_load(char**, int);
void ctl_save(char**, int);
void ctl_quit(char**, int);
void init_emoji(void);
void initcolors(void);
void update_formula_format(void);
void validate_config(void);
int cistrcmp(char *s1, char *s2);
int tokenize_formula(char *formula, Token *tokens, int maxtokens);
int cellref_lookup(char *ref);
int edit_finish(char *buf, int *pos, int maxlen);
int edit_cancel(char *buf, int *pos, int maxlen);
int edit_backspace(char *buf, int *pos, int maxlen);
int edit_add_char(char *buf, int *pos, int maxlen);
double round(double nargs);
double fn_sum(Token *args, int nargs);
double fn_avg(Token *args, int nargs);
double fn_min(Token *args, int nargs);
double fn_max(Token *args, int nargs);
double fn_count(Token *args, int nargs);
double fn_abs(Token *args, int nargs);
double fn_sqrt(Token *args, int nargs);
double fn_pow(Token *args, int nargs);
double fn_round(Token *args, int nargs);
double fn_floor(Token *args, int nargs);
double fn_ceil(Token *args, int nargs);
double fn_if(Token *args, int nargs);
double fn_and(Token *args, int nargs);
double fn_or(Token *args, int nargs);
double fn_not(Token *args, int nargs);
double fn_lookup(Token *args, int nargs);
double eval_expr(Eval *e);
double eval_term(Eval *e);
double eval_factor(Eval *e);
double eval_primary(Eval *e);

ConfigField config_fields[] = {
	{"banner_height",				CFG_INT,	&config.banner_height,				0,	nil},
	{"status_height",				CFG_INT,	&config.status_height,				0,	nil},
	{"box_label_offset_y",			CFG_INT,	&config.box_label_offset_y,			0,	nil},
	{"box_text_margin",				CFG_INT,	&config.box_text_margin,			0,	nil},
	{"formula_indicator_offset",	CFG_INT,	&config.formula_indicator_offset,	0,	nil},
	{"dialog_width",				CFG_INT,	&config.dialog_width,				0,	nil},
	{"dialog_height",				CFG_INT,	&config.dialog_height,				0,	nil},
	{"dialog_padding",				CFG_INT,	&config.dialog_padding,				0,	nil},
	{"paragraph_height",			CFG_INT,	&config.paragraph_height,			0,	nil},
	{"paragraph_width",				CFG_INT,	&config.paragraph_width,			0,	nil},
	{"emoji_enabled",				CFG_BOOL,	&config.emoji_enabled,				0,	nil},
	{"emoji_speed",					CFG_INT,	&config.emoji_speed,				0,	nil},
	{"emoji_frame_delay",			CFG_INT,	&config.emoji_frame_delay,			0,	nil},
	{"ctl_check_interval",			CFG_INT,	&config.ctl_check_interval,			0,	nil},
	{"redraw_interval",				CFG_INT,	&config.redraw_interval,			0,	nil},

	{"gridsize",					CFG_INT,	&config.gridsize,					0,	nil},
	{"gridsnap",					CFG_BOOL,	&config.gridsnap,					0,	nil},

	{"max_eval_depth",				CFG_INT,	&config.max_eval_depth,				0,	nil},
	{"max_recalc_passes",			CFG_INT,	&config.max_recalc_passes,			0,	nil},
	{"formula_precision",			CFG_INT,	&config.formula_precision,			0,	update_formula_format},
	{"show_formula_indicator",		CFG_BOOL,	&config.show_formula_indicator,		0,	nil},

	{"ctl_file",					CFG_STRING,	config.ctl_file,					256, nil},
	{"default_save",				CFG_STRING,	config.default_save_path,			256, nil},
	{"color_bg",					CFG_COLOR,	&config.color_bg,					0, nil},
	{"color_fg",					CFG_COLOR,	&config.color_fg,					0, nil},
	{"color_box_bg",				CFG_COLOR,	&config.color_box_bg,				0, nil},
	{"color_selected",				CFG_COLOR,	&config.color_selected,				0, nil},
	{"color_editing",				CFG_COLOR,	&config.color_editing,				0, nil},
	{"color_grid",					CFG_COLOR,	&config.color_grid,					0, nil},
	{"color_label",					CFG_COLOR,	&config.color_label,				0, nil},
	{"color_formula",				CFG_COLOR,	&config.color_formula,				0, nil},
	{nil, 0, nil, 0, nil} /* sentinel */
};

BoxType boxtypes[] = {
	[T_TEXT]	= {"text",	parse_text,	eval_text,	draw_box_generic},
	[T_NUMBER]  = {"number",  parse_number,  eval_number,  draw_box_generic},
	[T_FORMULA] = {"formula", parse_formula, eval_formula, draw_box_generic},
	[T_LABEL]   = {"label",   parse_text,	eval_text,	draw_box_generic},
	[T_PARAGRAPH] = {"paragraph", parse_paragraph, eval_paragraph, draw_box_paragraph},
};

Function functions[MAXFUNCS] = {
	[FN_SUM]	= {"SUM",	1, 100, fn_sum},
	[FN_AVG]	= {"AVG",	1, 100, fn_avg},
	[FN_MIN]	= {"MIN",	1, 100, fn_min},
	[FN_MAX]	= {"MAX",	1, 100, fn_max},
	[FN_COUNT]  = {"COUNT",  1, 100, fn_count},
	[FN_ABS]	= {"ABS",	1, 1,   fn_abs},
	[FN_SQRT]   = {"SQRT",   1, 1,   fn_sqrt},
	[FN_POW]	= {"POW",	2, 2,   fn_pow},
	[FN_ROUND]  = {"ROUND",  1, 2,   fn_round},
	[FN_FLOOR]  = {"FLOOR",  1, 1,   fn_floor},
	[FN_CEIL]   = {"CEIL",   1, 1,   fn_ceil},
	[FN_IF]	 = {"IF",	 3, 3,   fn_if},
	[FN_AND]	= {"AND",	2, 100, fn_and},
	[FN_OR]	 = {"OR",	 2, 100, fn_or},
	[FN_NOT]	= {"NOT",	1, 1,   fn_not},
	[FN_LOOKUP] = {"LOOKUP", 2, 3,   fn_lookup},
};

InputMode input_modes[] = {
	[0] = {"normal",	handle_normal_mode,	handle_normal_mouse,	draw_normal_overlay,	"E:moji  e:cycle  S:ave  O:pen  l:abel  g:rid  r:eload-cfg "},
	[1] = {"edit",	  handle_cell_edit,	  handle_edit_mouse,	  draw_cell_edit_overlay, "Enter:save  Esc:cancel"},
	[2] = {"label",	 handle_label_edit,	 handle_label_mouse,	 draw_label_edit_overlay,"Enter:save  Esc:cancel"},
	[3] = {"filename",  handle_filename_input, handle_filename_mouse,  draw_filename_overlay,  "Tab:.spr  Enter:confirm  Esc:cancel"},
};

Command commands[] = {
	{'q',	cmd_quit},
	{Kdel,	cmd_quit},
	{'s',	cmd_save},
	{'S',	cmd_save_as},
	{'o',	cmd_open},
	{'O',	cmd_open_file},
	{'l',	cmd_start_label},
	{'g',	cmd_toggle_grid},
	{'d',	cmd_delete_box},
	{'E',   cmd_toggle_emoji},
	{'e',   cmd_cycle_emoji},
	{'r',   cmd_reload_config},
	{0, nil}
};

EditAction edit_actions[] = {
	{'\n',	edit_finish},
	{Kesc,	edit_cancel},
	{Kbs,	edit_backspace},
	{-1,	edit_add_char},
	{0, nil}
};

DrawStep draw_steps[] = {
	{draw_background,	0},
	{draw_grid_lines,	1},
	{draw_all_boxes,	0},
	{draw_status_line,	0},
	{nil, 0}
};

CommandHandler cmd_handlers[] = {
	{"addbox", 2, ctl_addbox},
	{"load",   1, ctl_load},
	{"save",   1, ctl_save},
	{"quit",   0, ctl_quit},
	{nil, 0, nil}
};

char *happy_faces[] = {
	"^_^",
	"^o^",
	"^_^",
	"^-^"
};

char *dancing_guy[] = {
	"\\o/",
	"_o_",
	"/o\\",
	"_o_"
};

char *kirby_dance[] = {
	"<('.')>",
	"<('.')<",
	"^('.')^",
	"v('.')v"
};

char *lambda_dance[] = {
	"L(^_^)L",
	"L(>_<)L",
	"L(o_o)L",
	"L(*_*)L"
};

char *rcc_style[] = {
	"(-(-_-(-_(-_-)_-)_-)-)",
	"[~o-o]~",
	"~(o_o)~",
	"*(^o^)/*"
};

char *cat_faces[] = {
	"=^.^=",
	"=^.o=",
	"=o.^=",
	"=o.o="
};

char *shrug_guys[] = {
	"~\\_('.')_/~",
	"~\\_(o.o)_/~",
	"~\\_(-.-)_/~",
	"~\\_(^.^)_/~"
};

void
save_spr(char *file)
{
	int fd;
	Biobuf *b;
	int i;
	Box *box;

	fd = create(file, OWRITE, 0644);
	if(fd < 0){
		fprint(2, "cannot create %s: %r\n", file);
		return;
	}

	b = Bfdopen(fd, OWRITE);

	for(i = 0; i < sheet.nboxes; i++){
		box = &sheet.boxes[i];
		Bprint(b, "box %d\n", i);
		Bprint(b, "  pos %d %d\n", box->pos.x, box->pos.y);
		Bprint(b, "  type %d\n", box->type);
		
		Bprint(b, "  formula_len %d\n", (int)strlen(box->formula));
		Bprint(b, "  formula %s\n", box->formula);
		
		Bprint(b, "  value %g\n", box->value);
		Bprint(b, "  label_len %d\n", (int)strlen(box->label));
		Bprint(b, "  label %s\n", box->label);

		if(box->nrefs > 0){
			Bprint(b, "  refs");
			int j;
			for(j = 0; j < box->nrefs; j++)
				Bprint(b, " %d", box->refs[j]);
			Bprint(b, "\n");
		}
	}

	Bterm(b);
	close(fd);
}

void
load_spr(char *file)
{
	Biobuf *b;
	char *line;
	char *fields[10];
	int nf;
	int formula_len = 0;
	int label_len = 0;

	b = Bopen(file, OREAD);
	if(b == nil){
		fprint(2, "cannot open %s: %r\n", file);
		return;
	}

	memset(&sheet, 0, sizeof(sheet));
	sheet.selected = -1;
	sheet.editing = -1;
	sheet.editing_label = -1;
	sheet.entering_filename = 0;
	sheet.current_mode = 0;
	sheet.gridsize = config.gridsize;
	sheet.gridsnap = config.gridsnap;
	init_emoji();

	while((line = Brdline(b, '\n')) != nil){
		line[Blinelen(b)-1] = '\0';

		if(line[0] == '#' || line[0] == '\0')
			continue;

		// Check for formula with spaces (don't tokenize these lines)
		if(strncmp(line, "  formula ", 10) == 0){
			if(sheet.nboxes > 0){
				Box *box = &sheet.boxes[sheet.nboxes-1];
				char *content = line + 10;
				
				// If we have a length, use it; otherwise just copy what's there
				if(formula_len > 0 && formula_len < MAXFORMULA){
					strncpy(box->formula, content, formula_len);
					box->formula[formula_len] = '\0';
					formula_len = 0;
				} else {
					strncpy(box->formula, content, MAXFORMULA-1);
					box->formula[MAXFORMULA-1] = '\0';
				}
			}
			continue;
		}
		
		// Check for label with spaces
		if(strncmp(line, "  label ", 8) == 0){
			if(sheet.nboxes > 0){
				Box *box = &sheet.boxes[sheet.nboxes-1];
				char *content = line + 8;
				
				if(label_len > 0 && label_len < 32){
					strncpy(box->label, content, label_len);
					box->label[label_len] = '\0';
					label_len = 0;
				} else {
					strncpy(box->label, content, 31);
					box->label[31] = '\0';
				}
			}
			continue;
		}

		// For other lines, use tokenize as before
		nf = tokenize(line, fields, nelem(fields));

		if(nf >= 2 && strcmp(fields[0], "box") == 0){
			if(sheet.nboxes < MAXBOXES){
				Box *box = &sheet.boxes[sheet.nboxes];
				memset(box, 0, sizeof(Box));
				box->r = Rect(0, 0, BOXWIDTH, BOXHEIGHT);
				sheet.nboxes++;
			}
		} else if(nf >= 3 && strcmp(fields[0], "pos") == 0){
			Box *box = &sheet.boxes[sheet.nboxes-1];
			box->pos.x = atoi(fields[1]);
			box->pos.y = atoi(fields[2]);
			box->r = Rect(box->pos.x, box->pos.y,
				box->pos.x + BOXWIDTH, box->pos.y + BOXHEIGHT);
		} else if(nf >= 2 && strcmp(fields[0], "type") == 0){
			int type = atoi(fields[1]);
			if (type >= 0 && type < MAXBOXTYPES) {
				Box *box = &sheet.boxes[sheet.nboxes-1];
				box->type = type;
				
				// Resize box if it's a paragraph
				if(type == T_PARAGRAPH){
					box->r = Rect(box->pos.x, box->pos.y, 
						box->pos.x + config.paragraph_width, 
						box->pos.y + config.paragraph_height);
				}
			}
		} else if(nf >= 2 && strcmp(fields[0], "formula_len") == 0){
			formula_len = atoi(fields[1]);
		} else if(nf >= 2 && strcmp(fields[0], "label_len") == 0){
			label_len = atoi(fields[1]);
		} else if(nf >= 2 && strcmp(fields[0], "value") == 0){
			sheet.boxes[sheet.nboxes-1].value = atof(fields[1]);
		}
	}

	Bterm(b);

	int i;
	for(i = 0; i < sheet.nboxes; i++){
		Box *box = &sheet.boxes[i];
		if (box->type >= 0 && box->type < MAXBOXTYPES) {
			BoxType *bt = &boxtypes[box->type];
			if (bt->parse) bt->parse(box);
			if (bt->eval) bt->eval(box);
		}
	}
	recalc_all();
	redraw();
}

void
save_config(char *path)
{
	int fd;
	Biobuf *b;
	
	fd = create(path, OWRITE, 0644);
	if(fd < 0) {
		fprint(2, "cannot create config %s: %r\n", path);
		return;
	}
	
	b = Bfdopen(fd, OWRITE);
	
	Bprint(b, "# fc.conf auto-gen\n");
	
	Bprint(b, "banner_height %d\n", config.banner_height);
	Bprint(b, "status_height %d\n", config.status_height);
	Bprint(b, "box_label_offset_y %d\n", config.box_label_offset_y);
	Bprint(b, "box_text_margin %d\n", config.box_text_margin);
	Bprint(b, "formula_indicator_offset %d\n", config.formula_indicator_offset);
	
	Bprint(b, "dialog_width %d\n", config.dialog_width);
	Bprint(b, "dialog_height %d\n", config.dialog_height);
	Bprint(b, "dialog_padding %d\n", config.dialog_padding);
	Bprint(b, "paragraph_height %d\n", config.paragraph_height);
	Bprint(b, "paragraph_width %d\n", config.paragraph_width);
	
	Bprint(b, "emoji_enabled %d\n", config.emoji_enabled);
	Bprint(b, "emoji_speed %d\n", config.emoji_speed);
	Bprint(b, "emoji_frame_delay %d\n", config.emoji_frame_delay);
	Bprint(b, "ctl_check_interval %d\n", config.ctl_check_interval);
	Bprint(b, "redraw_interval %d\n", config.redraw_interval);
	
	Bprint(b, "gridsize %d\n", config.gridsize);
	Bprint(b, "gridsnap %d\n", config.gridsnap);
	
	Bprint(b, "max_eval_depth %d\n", config.max_eval_depth);
	Bprint(b, "max_recalc_passes %d\n", config.max_recalc_passes);
	Bprint(b, "formula_precision %d\n", config.formula_precision);
	Bprint(b, "show_formula_indicator %d\n", config.show_formula_indicator);
	
	Bprint(b, "ctl_file %s\n", config.ctl_file);
	Bprint(b, "default_save %s\n", config.default_save_path);
	
	Bprint(b, "color_bg %08lux\n", config.color_bg);
	Bprint(b, "color_fg %08lux\n", config.color_fg);
	Bprint(b, "color_box_bg %08lux\n", config.color_box_bg);
	Bprint(b, "color_selected %08lux\n", config.color_selected);
	Bprint(b, "color_editing %08lux\n", config.color_editing);
	Bprint(b, "color_grid %08lux\n", config.color_grid);
	Bprint(b, "color_label %08lux\n", config.color_label);
	Bprint(b, "color_formula %08lux\n", config.color_formula);
	
	Bterm(b);
	close(fd);
}

void
init_config_defaults(void)
{
	config.banner_height = DEFAULT_BANNER_HEIGHT;
	config.status_height = DEFAULT_STATUS_HEIGHT;
	config.status_margin = DEFAULT_STATUS_MARGIN;
	config.box_label_offset_y = DEFAULT_BOX_LABEL_OFFSET_Y;
	config.box_text_margin = DEFAULT_BOX_TEXT_MARGIN;
	config.formula_indicator_offset = DEFAULT_FORMULA_INDICATOR_OFFSET;

	config.dialog_width = DEFAULT_DIALOG_WIDTH;
	config.dialog_height = DEFAULT_DIALOG_HEIGHT;
	config.dialog_padding = DEFAULT_DIALOG_PADDING;
	config.paragraph_height = DEFAULT_PARAGRAPH_H;
	config.paragraph_width = DEFAULT_PARAGRAPH_W;

	config.emoji_enabled = 1;
	config.emoji_speed = DEFAULT_EMOJI_SPEED;
	config.emoji_frame_delay = DEFAULT_EMOJI_FRAME_DELAY;
	config.ctl_check_interval = DEFAULT_CTL_CHECK_INTERVAL;
	config.redraw_interval = DEFAULT_REDRAW_INTERVAL;

	config.gridsnap = 1;
	config.gridsize = 32;

	config.max_eval_depth = DEFAULT_MAX_EVAL_DEPTH;
	config.max_recalc_passes = DEFAULT_MAX_RECALC_PASSES;
	config.formula_precision = DEFAULT_FORMULA_PRECISION;
	config.show_formula_indicator = 1;

	strcpy(config.ctl_file, DEFAULT_CTL_FILE);
	strcpy(config.default_save_path, DEFAULT_SAVE_PATH);

	config.color_bg = 0xEEEEEEFF;
	config.color_fg = 0x000000FF;
	config.color_box_bg = 0xFFFFFFFF;
	config.color_selected = 0x4444FFFF;
	config.color_editing = 0xCCCC88FF;
	config.color_grid = 0xCCCCCCFF;
	config.color_label = 0xFF4444FF;
	config.color_formula = 0x4444FFFF;

	update_formula_format();
}

void
update_formula_format(void)
{
	snprint(config.formula_format, sizeof(config.formula_format),
		"%%.%df", config.formula_precision);
}

void
load_config(char *path)
{
	Biobuf *b;
	char *line;
	char *fields[3];
	int nf;
	ConfigField *cf;
	init_config_defaults();

	b = Bopen(path, OREAD);
	if(b == nil)
		return;

	while((line = Brdline(b, '\n')) != nil) {
		line[Blinelen(b)-1] = '\0';

		if(line[0] == '#' || line[0] == '\0')
			continue;

		nf = tokenize(line, fields, nelem(fields));
		if(nf < 2)
			continue;

		for(cf = config_fields; cf->name != nil; cf++) {
			if(strcmp(fields[0], cf->name) == 0) {
				/* Process based on type */
				switch(cf->type) {
				case CFG_INT:
					*(int*)cf->ptr = atoi(fields[1]);
					break;

				case CFG_BOOL:
					*(int*)cf->ptr = atoi(fields[1]) ? 1 : 0;
					break;

				case CFG_STRING:
					strncpy((char*)cf->ptr, fields[1], cf->maxlen - 1);
					((char*)cf->ptr)[cf->maxlen - 1] = '\0';
					break;

				case CFG_COLOR:
					*(ulong*)cf->ptr = strtoul(fields[1], nil, 16);
					break;
				}

				if(cf->callback)
					cf->callback();

				break;	/* Found, next line */
			}
		}

		if(cf->name == nil) {
			fprint(2, "Warning: unknown config field '%s'\n", fields[0]);
		}
	}

	Bterm(b);
}

void
validate_config(void)
{
	if(config.banner_height < 0) config.banner_height = 0;
	if(config.banner_height > 100) config.banner_height = 100;
	
	if(config.status_height < 0) config.status_height = 0;
	if(config.status_height > 100) config.status_height = 100;
	
	if(config.status_margin < 0) config.status_margin = 0;
	if(config.status_margin > 50) config.status_margin = 50;
	
	if(config.box_label_offset_y < 0) config.box_label_offset_y = 0;
	if(config.box_label_offset_y > 50) config.box_label_offset_y = 50;
	
	if(config.box_text_margin < 0) config.box_text_margin = 0;
	if(config.box_text_margin > 20) config.box_text_margin = 20;
	
	if(config.formula_indicator_offset < 0) config.formula_indicator_offset = 0;
	if(config.formula_indicator_offset > 30) config.formula_indicator_offset = 30;
	
	if(config.dialog_width < 100) config.dialog_width = 100;
	if(config.dialog_width > 800) config.dialog_width = 800;
	
	if(config.dialog_height < 30) config.dialog_height = 30;
	if(config.dialog_height > 300) config.dialog_height = 300;
	
	if(config.dialog_padding < 0) config.dialog_padding = 0;
	if(config.dialog_padding > 50) config.dialog_padding = 50;
	
	if(config.paragraph_width < 64) config.paragraph_width = 64;
	if(config.paragraph_width > 512) config.paragraph_width = 512;
	
	if(config.paragraph_height < 32) config.paragraph_height = 32;
	if(config.paragraph_height > 512) config.paragraph_height = 512;
	
	if(config.gridsize < 8) config.gridsize = 8;
	if(config.gridsize > 256) config.gridsize = 256;
	
	if(config.emoji_speed < 1) config.emoji_speed = 1;
	if(config.emoji_speed > 20) config.emoji_speed = 20;
	
	if(config.emoji_frame_delay < 1) config.emoji_frame_delay = 1;
	if(config.emoji_frame_delay > 100) config.emoji_frame_delay = 100;
	
	if(config.ctl_check_interval < 10) config.ctl_check_interval = 10;
	if(config.ctl_check_interval > 1000) config.ctl_check_interval = 1000;
	
	if(config.redraw_interval < 10) config.redraw_interval = 10;
	if(config.redraw_interval > 1000) config.redraw_interval = 1000;
	
	if(config.formula_precision < 0) config.formula_precision = 0;
	if(config.formula_precision > 10) config.formula_precision = 10;
	
	if(config.max_eval_depth < 1) config.max_eval_depth = 1;
	if(config.max_eval_depth > 100) config.max_eval_depth = 100;
	
	if(config.max_recalc_passes < 1) config.max_recalc_passes = 1;
	if(config.max_recalc_passes > 100) config.max_recalc_passes = 100;
	
	if(config.ctl_file[0] == '\0')
		strcpy(config.ctl_file, DEFAULT_CTL_FILE);
	if(config.default_save_path[0] == '\0')
		strcpy(config.default_save_path, DEFAULT_SAVE_PATH);
	
	config.emoji_enabled = config.emoji_enabled ? 1 : 0;
	config.gridsnap = config.gridsnap ? 1 : 0;
	config.show_formula_indicator = config.show_formula_indicator ? 1 : 0;
	
	/* Note: Color values don't need validation as they're ulong hex values */
	/* Any value is technically valid for a color */
	
	update_formula_format();
}

void
apply_config(void)
{
	sheet.emoji_enabled = config.emoji_enabled;
	sheet.gridsize = config.gridsize;
	sheet.gridsnap = config.gridsnap;

	if(colors[0]) {
		freeimage(colors[0]);
		colors[0] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, config.color_fg);
	}
	if(colors[1]) {
		freeimage(colors[1]);
		colors[1] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, config.color_box_bg);
	}
	if(colors[2]) {
		freeimage(colors[2]);
		colors[2] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, config.color_bg);
	}
	if(colors[3]) {
		freeimage(colors[3]);
		colors[3] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, config.color_formula);
	}
	if(colors[4]) {
		freeimage(colors[4]);
		colors[4] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, config.color_label);
	}
	if(colors[5]) {
		freeimage(colors[5]);
		colors[5] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, config.color_editing);
	}

	if(boxselected) {
		freeimage(boxselected);
		boxselected = allocimage(display, Rect(0,0,1,1), screen->chan, 1, config.color_selected);
	}
	if(boxediting) {
		freeimage(boxediting);
		boxediting = allocimage(display, Rect(0,0,1,1), screen->chan, 1, config.color_editing);
	}
	if(gridcolor) {
		freeimage(gridcolor);
		gridcolor = allocimage(display, Rect(0,0,1,1), screen->chan, 1, config.color_grid);
	}
}

void
initcolors(void)
{
	colors[0] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, config.color_fg);
	colors[1] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, config.color_box_bg);
	colors[2] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, config.color_bg);
	colors[3] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, config.color_formula);
	colors[4] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, config.color_label);
	colors[5] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, config.color_editing);

	boxbg = colors[1];
	boxselected = allocimage(display, Rect(0,0,1,1), screen->chan, 1, config.color_selected);
	boxediting = allocimage(display, Rect(0,0,1,1), screen->chan, 1, config.color_editing);
	gridcolor = allocimage(display, Rect(0,0,1,1), screen->chan, 1, config.color_grid);
}

void
init_emoji(void)
{
	sheet.emoji_pos = 0;
	sheet.emoji_frame = 0;
	sheet.emoji_dir = 1;
	sheet.emoji_enabled = config.emoji_enabled;

	int i;
	for(i = 0; i < 4; i++)
		sheet.emoji_frames[i] = rcc_style[i];
}

double
round(double x)
{
	if(x >= 0)
		return floor(x + 0.5);
	else
		return ceil(x - 0.5);
}

int
cistrcmp(char *s1, char *s2)
{
	while(*s1 && *s2) {
		int c1 = toupper(*s1);
		int c2 = toupper(*s2);
		if(c1 != c2)
			return c1 - c2;
		s1++;
		s2++;
	}
	return *s1 - *s2;
}

int
cellref_lookup(char *ref)
{
	int i;

	for(i = 0; i < sheet.nboxes; i++) {
		if(sheet.boxes[i].label[0] &&
		   cistrcmp(sheet.boxes[i].label, ref) == 0) {
			return i;
		}
	}

	if(strlen(ref) == 1 && isalpha(ref[0])) {
		int idx = toupper(ref[0]) - 'A';
		if(idx >= 0 && idx < sheet.nboxes)
			return idx;
	}

	if(strlen(ref) == 2 && isalpha(ref[0]) && isalpha(ref[1])) {
		int idx = (toupper(ref[0]) - 'A' + 1) * 26 + (toupper(ref[1]) - 'A');
		if(idx >= 0 && idx < sheet.nboxes)
			return idx;
	}

	if(isalpha(ref[0])) {
		int col = 0;
		char *p = ref;
		while(*p && isalpha(*p)) {
			col = col * 26 + (toupper(*p) - 'A');
			p++;
		}
		if(col >= 0 && col < sheet.nboxes)
			return col;
	}

	return -1;
}

int
tokenize_formula(char *formula, Token *tokens, int maxtokens)
{
	char *p = formula;
	int ntok = 0;
	char buf[256];
	int i;

	if(*p == '=')
		p++;

	while(*p && ntok < maxtokens) {
		Token *t = &tokens[ntok];

		while(*p && (*p == ' ' || *p == '\t'))
			p++;

		if(!*p)
			break;

		if(isdigit(*p) || (*p == '.' && isdigit(*(p+1)))) {
			char *endp;
			t->type = TOK_NUM;
			t->num = strtod(p, &endp);
			p = endp;
			ntok++;
			continue;
		}

		if(isalpha(*p)) {
			i = 0;
			while(*p && (isalnum(*p) || *p == '_') && i < 255)
				buf[i++] = *p++;
			buf[i] = '\0';

			if(*p == ':') {
				p++;
				char buf2[256];
				i = 0;
				while(*p && (isalnum(*p) || *p == '_'))
					buf2[i++] = *p++;
				buf2[i] = '\0';

				t->type = TOK_RANGE;
				t->range.start = cellref_lookup(buf);
				t->range.end = cellref_lookup(buf2);
				ntok++;
				continue;
			}

			int found = 0;
			for(i = 0; i < MAXFUNCS; i++) {
				if(functions[i].name && cistrcmp(buf, functions[i].name) == 0) {
					t->type = TOK_FUNC;
					t->func = i;
					found = 1;
					break;
				}
			}

			if(!found) {
				t->type = TOK_CELL;
				t->cell = cellref_lookup(buf);
			}
			ntok++;
			continue;
		}

		if(*p == '"') {
			p++;
			i = 0;
			while(*p && *p != '"' && i < MAXCONTENT-1)
				t->str[i++] = *p++;
			t->str[i] = '\0';
			if(*p == '"')
				p++;
			t->type = TOK_STRING;
			ntok++;
			continue;
		}

		switch(*p) {
		case '+': case '-': case '*': case '/': case '%': case '^':
		case '=': case '<': case '>':
			t->type = TOK_OP;
			t->op = *p++;

			if(t->op == '<' && *p == '=') {
				t->op = OP_LE;
				p++;
			} else if(t->op == '>' && *p == '=') {
				t->op = OP_GE;
				p++;
			} else if(t->op == '!' && *p == '=') {
				t->op = OP_NE;
				p++;
			}
			ntok++;
			break;

		case '(':
			t->type = TOK_LPAREN;
			p++;
			ntok++;
			break;

		case ')':
			t->type = TOK_RPAREN;
			p++;
			ntok++;
			break;

		case ',':
			t->type = TOK_COMMA;
			p++;
			ntok++;
			break;

		default:
			p++;
			break;
		}
	}

	if(ntok < maxtokens) {
		tokens[ntok].type = TOK_END;
		ntok++;
	}
	return ntok;
}

double
token_value(Token *t, Eval *e)
{
	Box *b;

	switch(t->type) {
	case TOK_NUM:
		return t->num;

	case TOK_CELL:
		if(t->cell >= 0 && t->cell < sheet.nboxes) {
			b = &sheet.boxes[t->cell];

			if(b == e->current) {
				return 0.0;
			}

			if(b->type == T_FORMULA && e->depth < config.max_eval_depth) {
				Eval subeval;
				subeval.current = b;
				subeval.depth = e->depth + 1;
				subeval.pos = 0;
				subeval.ntokens = tokenize_formula(b->formula,
					subeval.tokens, nelem(subeval.tokens));
				return eval_expr(&subeval);
			}
			return b->value;
		}
		return 0.0;

	case TOK_STRING:
		return atof(t->str);

	default:
		return 0.0;
	}
}

double
eval_primary(Eval *e)
{
	Token *t;
	double result = 0.0;

	if(e->pos >= e->ntokens)
		return 0.0;

	t = &e->tokens[e->pos];

	switch(t->type) {
	case TOK_NUM:
	case TOK_CELL:
	case TOK_STRING:
		result = token_value(t, e);
		e->pos++;
		break;

	case TOK_FUNC: {
		int func = t->func;
		e->pos++;

		if(e->pos >= e->ntokens || e->tokens[e->pos].type != TOK_LPAREN)
			return 0.0;
		e->pos++;

		Token args[100];
		int nargs = 0;

		while(e->pos < e->ntokens && e->tokens[e->pos].type != TOK_RPAREN) {
			if(nargs > 0) {
				if(e->tokens[e->pos].type != TOK_COMMA)
					break;
				e->pos++;
			}

			if(e->tokens[e->pos].type == TOK_RANGE) {
				int start = e->tokens[e->pos].range.start;
				int end = e->tokens[e->pos].range.end;
				e->pos++;

				int i;
				for(i = start; i <= end && i >= 0 && nargs < 100; i++) {
					args[nargs].type = TOK_CELL;
					args[nargs].cell = i;
					nargs++;
				}
			} else {
				args[nargs].type = TOK_NUM;
				args[nargs].num = eval_expr(e);
				nargs++;
			}
		}

		if(e->pos < e->ntokens && e->tokens[e->pos].type == TOK_RPAREN)
			e->pos++;

		if(func >= 0 && func < MAXFUNCS && functions[func].eval) {
			if(nargs >= functions[func].minargs && nargs <= functions[func].maxargs) {
				result = functions[func].eval(args, nargs);
			}
		}
		break;
	}

	case TOK_LPAREN:
		e->pos++;
		result = eval_expr(e);
		if(e->pos < e->ntokens && e->tokens[e->pos].type == TOK_RPAREN)
			e->pos++;
		break;

	case TOK_OP:
		if(t->op == OP_SUB) {
			e->pos++;
			result = -eval_primary(e);
		}
		break;
	}

	return result;
}

double
eval_factor(Eval *e)
{
	double left = eval_primary(e);

	while(e->pos < e->ntokens) {
		Token *t = &e->tokens[e->pos];
		if(t->type != TOK_OP || t->op != OP_POW)
			break;

		e->pos++;
		double right = eval_primary(e);
		left = pow(left, right);
	}

	return left;
}

double
eval_term(Eval *e)
{
	double left = eval_factor(e);

	while(e->pos < e->ntokens) {
		Token *t = &e->tokens[e->pos];
		if(t->type != TOK_OP)
			break;

		switch(t->op) {
		case OP_MUL:
			e->pos++;
			left *= eval_factor(e);
			break;
		case OP_DIV:
			e->pos++;
			{
				double right = eval_factor(e);
				if(right != 0.0)
					left /= right;
				else
					left = 0.0;
			}
			break;
		case OP_MOD:
			e->pos++;
			{
				double right = eval_factor(e);
				if(right != 0.0)
					left = fmod(left, right);
			}
			break;
		default:
			return left;
		}
	}

	return left;
}

double
eval_expr(Eval *e)
{
	double left = eval_term(e);

	while(e->pos < e->ntokens) {
		Token *t = &e->tokens[e->pos];
		if(t->type != TOK_OP)
			break;

		switch(t->op) {
		case OP_ADD:
			e->pos++;
			left += eval_term(e);
			break;
		case OP_SUB:
			e->pos++;
			left -= eval_term(e);
			break;
		case OP_LT:
			e->pos++;
			left = left < eval_term(e) ? 1.0 : 0.0;
			break;
		case OP_GT:
			e->pos++;
			left = left > eval_term(e) ? 1.0 : 0.0;
			break;
		case OP_LE:
			e->pos++;
			left = left <= eval_term(e) ? 1.0 : 0.0;
			break;
		case OP_GE:
			e->pos++;
			left = left >= eval_term(e) ? 1.0 : 0.0;
			break;
		case OP_EQ:
			e->pos++;
			left = fabs(left - eval_term(e)) < 0.000001 ? 1.0 : 0.0;
			break;
		case OP_NE:
			e->pos++;
			left = fabs(left - eval_term(e)) >= 0.000001 ? 1.0 : 0.0;
			break;
		default:
			return left;
		}
	}

	return left;
}

double
fn_sum(Token *args, int nargs)
{
	double sum = 0.0;
	int i;
	Eval e;

	for(i = 0; i < nargs; i++) {
		if(args[i].type == TOK_CELL) {
			e.current = nil;
			e.depth = 0;
			sum += token_value(&args[i], &e);
		} else {
			sum += args[i].num;
		}
	}
	return sum;
}

double
fn_avg(Token *args, int nargs)
{
	if(nargs == 0)
		return 0.0;
	return fn_sum(args, nargs) / nargs;
}

double
fn_min(Token *args, int nargs)
{
	if(nargs == 0)
		return 0.0;

	double min = 1e308;
	int i;
	Eval e;

	for(i = 0; i < nargs; i++) {
		double val;
		if(args[i].type == TOK_CELL) {
			e.current = nil;
			e.depth = 0;
			val = token_value(&args[i], &e);
		} else {
			val = args[i].num;
		}
		if(val < min)
			min = val;
	}
	return min;
}

double
fn_max(Token *args, int nargs)
{
	if(nargs == 0)
		return 0.0;

	double max = -1e308;
	int i;
	Eval e;

	for(i = 0; i < nargs; i++) {
		double val;
		if(args[i].type == TOK_CELL) {
			e.current = nil;
			e.depth = 0;
			val = token_value(&args[i], &e);
		} else {
			val = args[i].num;
		}
		if(val > max)
			max = val;
	}
	return max;
}

double
fn_count(Token *args, int nargs)
{
	int count = 0;
	int i;

	for(i = 0; i < nargs; i++) {
		if(args[i].type == TOK_CELL) {
			if(args[i].cell >= 0 && args[i].cell < sheet.nboxes) {
				Box *b = &sheet.boxes[args[i].cell];
				if(b->type == T_NUMBER || b->type == T_FORMULA)
					count++;
			}
		} else {
			count++;
		}
	}
	return (double)count;
}

double
fn_abs(Token *args, int nargs)
{
	USED(nargs);
	return fabs(args[0].num);
}

double
fn_sqrt(Token *args, int nargs)
{
	USED(nargs);
	return sqrt(args[0].num);
}

double
fn_pow(Token *args, int nargs)
{
	USED(nargs);
	return pow(args[0].num, args[1].num);
}

double
fn_round(Token *args, int nargs)
{
	if(nargs == 1) {
		return round(args[0].num);
	} else {
		double mult = pow(10, args[1].num);
		return round(args[0].num * mult) / mult;
	}
}

double
fn_floor(Token *args, int nargs)
{
	USED(nargs);
	return floor(args[0].num);
}

double
fn_ceil(Token *args, int nargs)
{
	USED(nargs);
	return ceil(args[0].num);
}

double
fn_if(Token *args, int nargs)
{
	USED(nargs);
	return args[0].num != 0.0 ? args[1].num : args[2].num;
}

double
fn_and(Token *args, int nargs)
{
	int i;
	for(i = 0; i < nargs; i++) {
		if(args[i].num == 0.0)
			return 0.0;
	}
	return 1.0;
}

double
fn_or(Token *args, int nargs)
{
	int i;
	for(i = 0; i < nargs; i++) {
		if(args[i].num != 0.0)
			return 1.0;
	}
	return 0.0;
}

double
fn_not(Token *args, int nargs)
{
	USED(nargs);
	return args[0].num == 0.0 ? 1.0 : 0.0;
}

double
fn_lookup(Token *args, int nargs)
{
	if(nargs < 2)
		return 0.0;
	return args[0].num;
}

void
parse_formula(Box *b)
{
	Eval e;

	if(b->formula[0] != '=') {
		char *endp;
		double val = strtod(b->formula, &endp);
		if(*endp == '\0') {
			b->type = T_NUMBER;
			b->value = val;
			snprint(b->content, MAXCONTENT, config.formula_format, val);
		} else {
			b->type = T_TEXT;
			strncpy(b->content, b->formula, MAXCONTENT);
		}
		return;
	}

	b->type = T_FORMULA;

	e.current = b;
	e.depth = 0;
	e.pos = 0;
	e.ntokens = tokenize_formula(b->formula, e.tokens, nelem(e.tokens));
	strncpy(b->content, b->formula, MAXCONTENT);
	b->dirty = 1;

	b->nrefs = 0;
	int i;
	for(i = 0; i < e.ntokens && b->nrefs < 10; i++) {
		if(e.tokens[i].type == TOK_CELL && e.tokens[i].cell >= 0) {
			b->refs[b->nrefs++] = e.tokens[i].cell;
		} else if(e.tokens[i].type == TOK_RANGE) {
			if(b->nrefs < 10 && e.tokens[i].range.start >= 0)
				b->refs[b->nrefs++] = e.tokens[i].range.start;
			if(b->nrefs < 10 && e.tokens[i].range.end >= 0)
				b->refs[b->nrefs++] = e.tokens[i].range.end;
		}
	}
}

void
eval_formula(Box *b)
{
	Eval e;

	if(b->type != T_FORMULA || !b->dirty)
		return;

	e.current = b;
	e.depth = 0;
	e.pos = 0;
	e.ntokens = tokenize_formula(b->formula, e.tokens, nelem(e.tokens));

	b->value = eval_expr(&e);

	if(b->formula[0] == '=' && b->formula[1] == '"') {
		strncpy(b->content, b->formula + 2, MAXCONTENT);
		char *p = strchr(b->content, '"');
		if(p) *p = '\0';
	} else {
		snprint(b->content, MAXCONTENT, config.formula_format, b->value);
	}

	b->dirty = 0;

	int i, j;
	for(i = 0; i < sheet.nboxes; i++) {
		Box *other = &sheet.boxes[i];
		if(other->type == T_FORMULA) {
			for(j = 0; j < other->nrefs; j++) {
				if(other->refs[j] == b - sheet.boxes) {
					other->dirty = 1;
					break;
				}
			}
		}
	}
}

void
recalc_all(void)
{
	int i, changed;
	int passes = 0;

	for(i = 0; i < sheet.nboxes; i++) {
		if(sheet.boxes[i].type == T_FORMULA)
			sheet.boxes[i].dirty = 1;
	}

	do {
		changed = 0;
		for(i = 0; i < sheet.nboxes; i++) {
			Box *b = &sheet.boxes[i];
			if(b->type == T_FORMULA && b->dirty) {
				eval_formula(b);
				changed = 1;
			}
		}
		passes++;
	} while(changed && passes < config.max_recalc_passes);
}

int
boxat(Point p)
{
	int i;
	Box *b;

	for(i = sheet.nboxes - 1; i >= 0; i--){
		b = &sheet.boxes[i];
		if(ptinrect(p, b->r))
			return i;
	}
	return -1;
}

int
addbox(Point p)
{
	Box *b;

	if(sheet.nboxes >= MAXBOXES)
		return -1;

	b = &sheet.boxes[sheet.nboxes];
	memset(b, 0, sizeof(Box));

	if(sheet.gridsnap){
		p.x = (p.x / sheet.gridsize) * sheet.gridsize;
		p.y = (p.y / sheet.gridsize) * sheet.gridsize;
	}

	b->pos = p;
	b->r = Rect(p.x, p.y, p.x + BOXWIDTH, p.y + BOXHEIGHT);
	b->type = T_TEXT;
	strcpy(b->content, "");

	return sheet.nboxes++;
}

void
delbox(int i)
{
	if(i < 0 || i >= sheet.nboxes)
		return;

	memmove(&sheet.boxes[i], &sheet.boxes[i+1],
		(sheet.nboxes - i - 1) * sizeof(Box));
	sheet.nboxes--;

	int j, k;
	for(j = 0; j < sheet.nboxes; j++){
		Box *b = &sheet.boxes[j];
		for(k = 0; k < b->nrefs; k++){
			if(b->refs[k] > i)
				b->refs[k]--;
			else if(b->refs[k] == i)
				b->refs[k] = -1;
		}
	}
}

void
parse_paragraph(Box *b)
{
	if(b->formula[0] == '"'){
		strncpy(b->content, b->formula + 1, MAXCONTENT - 1);
	} else {
		strncpy(b->content, b->formula, MAXCONTENT - 1);
	}
	b->content[MAXCONTENT-1] = '\0';
}

void
parse_text(Box *b)
{
	strncpy(b->content, b->formula, MAXCONTENT);
}

void
parse_number(Box *b)
{
	char *endp;
	b->value = strtod(b->formula, &endp);
	snprint(b->content, MAXCONTENT, config.formula_format, b->value);
}

void
eval_text(Box *b)
{
	USED(b);
}

void
eval_number(Box *b)
{
	USED(b);
}

void
eval_paragraph(Box *b)
{
	USED(b);
}

void
draw_box_generic(Box *b, Image *dst)
{
	Image *bg = boxbg;
	int idx = b - sheet.boxes;

	if(sheet.editing == idx)
		bg = boxediting;
	else if(sheet.editing_label == idx)
		bg = colors[5];
	else if(b->selected)
		bg = boxselected;

	draw(dst, b->r, bg, nil, ZP);
	border(dst, b->r, 1, colors[0], ZP);

	char cellname[32];
	if(sheet.editing_label == idx){
		snprint(cellname, sizeof(cellname), "%s", sheet.labelbuf);
		string(dst, Pt(b->r.min.x + 2, b->r.min.y + 2), colors[4], ZP, font, cellname);
	} else if(b->label[0]) {
		snprint(cellname, sizeof(cellname), "%s", b->label);
		string(dst, Pt(b->r.min.x + 2, b->r.min.y + 2), colors[4], ZP, font, cellname);
	} else {
		if(idx < 26) {
			snprint(cellname, sizeof(cellname), "%c", 'A' + idx);
		} else {
			snprint(cellname, sizeof(cellname), "%c%c",
				'A' + (idx/26)-1, 'A' + (idx%26));
		}
		string(dst, Pt(b->r.min.x + 2, b->r.min.y + 2), colors[3], ZP, font, cellname);
	}

	Point p = addpt(b->r.min, Pt(config.box_text_margin, config.box_label_offset_y));

	if(sheet.editing == idx){
		string(dst, p, colors[0], ZP, font, sheet.editbuf);
	} else {
		string(dst, p, colors[0], ZP, font, b->content);
	}

	if (b->type == T_FORMULA && config.show_formula_indicator){
		string(dst, Pt(b->r.max.x - config.formula_indicator_offset, b->r.min.y + 2),
			colors[3], ZP, font, "=");
	}
}

void
drawgrid(Image *dst)
{
	int x, y;
	Rectangle r = screen->r;

	int startx = (r.min.x / sheet.gridsize) * sheet.gridsize;
	int starty = (r.min.y / sheet.gridsize) * sheet.gridsize;

	for(x = startx; x <= r.max.x; x += sheet.gridsize){
		line(dst, Pt(x, r.min.y), Pt(x, r.max.y), 0, 0, 0, gridcolor, ZP);
	}

	for(y = starty; y <= r.max.y; y += sheet.gridsize){
		line(dst, Pt(r.min.x, y), Pt(r.max.x, y), 0, 0, 0, gridcolor, ZP);
	}
}

void
draw_normal_overlay(void)
{
}

void
draw_cell_edit_overlay(void)
{
}

void
draw_label_edit_overlay(void)
{
}

void
draw_filename_overlay(void)
{
	Rectangle r;
	int w = config.dialog_width;
	int h = config.dialog_height;
	Point center = Pt(screen->r.min.x + Dx(screen->r)/2,
			  screen->r.min.y + Dy(screen->r)/2);

	r = Rect(center.x - w/2, center.y - h/2,
		 center.x + w/2, center.y + h/2);

	draw(screen, r, colors[1], nil, ZP);
	border(screen, r, 2, colors[0], ZP);

	char *title = sheet.save_mode == 1 ? "Save As:" : "Open File:";
	string(screen, Pt(r.min.x + config.dialog_padding, r.min.y + 5),
		colors[0], ZP, font, title);

	char display[256];
	snprint(display, sizeof(display), "%s_", sheet.filenamebuf);
	string(screen, Pt(r.min.x + config.dialog_padding, r.min.y + 20),
		colors[0], ZP, font, display);
}

void
draw_background(void)
{
	draw(screen, screen->r, colors[2], nil, ZP);
}

void
draw_grid_lines(void)
{
	if(!sheet.gridsnap)
		return;
	drawgrid(screen);
}

void
draw_all_boxes(void)
{
	int i;
	for(i = 0; i < sheet.nboxes; i++) {
		Box *b = &sheet.boxes[i];
		if (b->type >= 0 && b->type < MAXBOXTYPES) {
			BoxType *bt = &boxtypes[b->type];
			if (bt->draw) {
				bt->draw(b, screen);
			}
		}
	}
}

void
draw_status_line(void)
{
	char buf[256];
	InputMode *mode = &input_modes[sheet.current_mode];

	snprint(buf, sizeof(buf), "Selected: %d | Mode: %s | Boxes: %d | %s",
			sheet.selected, mode->name, sheet.nboxes, mode->status);

	string(screen, Pt(screen->r.min.x + config.status_margin,
		screen->r.max.y - config.status_height),
		colors[0], ZP, font, buf);
}

void
draw_emoji_banner(void)
{
	if(!sheet.emoji_enabled)
		return;

	if(sheet.emoji_frame < 0 || sheet.emoji_frame >= 4)
		sheet.emoji_frame = 0;

	char *emoji = sheet.emoji_frames[sheet.emoji_frame];
	if(emoji == nil)
		return;

	int emoji_width = strlen(emoji) * font->width;

	Rectangle banner = Rect(screen->r.min.x, screen->r.min.y,
						   screen->r.max.x, screen->r.min.y + config.banner_height);
	draw(screen, banner, colors[2], nil, ZP);

	Point pos = Pt(sheet.emoji_pos, screen->r.min.y + 5);
	string(screen, pos, colors[0], ZP, font, emoji);

	sheet.emoji_pos += sheet.emoji_dir * config.emoji_speed;

	if(sheet.emoji_pos > screen->r.max.x - emoji_width) {
		sheet.emoji_pos = screen->r.max.x - emoji_width;
		sheet.emoji_dir = -1;
	}
	if(sheet.emoji_pos < screen->r.min.x) {
		sheet.emoji_pos = screen->r.min.x;
		sheet.emoji_dir = 1;
	}

	static int frame_counter = 0;
	frame_counter++;
	if(frame_counter % config.emoji_frame_delay == 0) {
		sheet.emoji_frame = (sheet.emoji_frame + 1) % 4;
	}
}

void
draw_box_paragraph(Box *b, Image *dst)
{
	Image *bg = boxbg;
	int idx = b - sheet.boxes;

	/* Determine background color based on state (editing, selected) */
	if(sheet.editing == idx)
		bg = boxediting;
	else if(sheet.editing_label == idx)
		bg = colors[5];
	else if(b->selected)
		bg = boxselected;

	/* Draw box background and border */
	draw(dst, b->r, bg, nil, ZP);
	border(dst, b->r, 1, colors[0], ZP);

	/* Draw the cell's label or default name */
	char cellname[32];
	if(sheet.editing_label == idx){
		snprint(cellname, sizeof(cellname), "%s", sheet.labelbuf);
		string(dst, Pt(b->r.min.x + 2, b->r.min.y + 2), colors[4], ZP, font, cellname);
	} else if(b->label[0]) {
		snprint(cellname, sizeof(cellname), "%s", b->label);
		string(dst, Pt(b->r.min.x + 2, b->r.min.y + 2), colors[4], ZP, font, cellname);
	} else {
		if(idx < 26) {
			snprint(cellname, sizeof(cellname), "%c", 'A' + idx);
		} else {
			snprint(cellname, sizeof(cellname), "%c%c", 'A' + (idx/26)-1, 'A' + (idx%26));
		}
		string(dst, Pt(b->r.min.x + 2, b->r.min.y + 2), colors[3], ZP, font, cellname);
	}

	/* Text wrapping logic */
	Point p = addpt(b->r.min, Pt(config.box_text_margin, config.box_label_offset_y));
	char *text_to_draw = (sheet.editing == idx) ? sheet.editbuf : b->content;
	int max_width = Dx(b->r) - (2 * config.box_text_margin);
	int line_height = font->height;

	char *text = text_to_draw;
	char *line_start = text;
	char line_buffer[MAXCONTENT];

	while (*line_start) {
		char *word_ptr = line_start;
		char *line_end = line_start;
		
		while (*word_ptr) {
			char *word_start = word_ptr;
			while(*word_ptr && !isspace(*word_ptr))
				word_ptr++;
			
			int word_len = word_ptr - line_start;
			if(word_len >= MAXCONTENT) break;

			strncpy(line_buffer, line_start, word_len);
			line_buffer[word_len] = '\0';

			if(stringwidth(font, line_buffer) > max_width) {
				if(line_end == line_start)
					line_end = word_start;
				break;
			}
			line_end = word_ptr;

			while(*word_ptr && isspace(*word_ptr))
				word_ptr++;
		}
		
		int len = line_end - line_start;
		stringn(dst, p, colors[0], ZP, font, line_start, len);
		p.y += line_height;
		
		if (p.y > b->r.max.y - config.box_text_margin - line_height)
			break;

		line_start = line_end;
		while(*line_start && isspace(*line_start))
			line_start++;
	}
}

void
redraw(void)
{
	DrawStep *step;
	draw_emoji_banner();
	Rectangle clip = screen->r;
	clip.min.y += config.banner_height;
	replclipr(screen, 0, clip);

	for(step = draw_steps; step->draw; step++) {
		if(step->condition == 0 ||
		   (step->condition == 1 && sheet.gridsnap)) {
			step->draw();
		}
	}
	replclipr(screen, 0, screen->r);

	InputMode *mode = &input_modes[sheet.current_mode];
	if(mode->draw)
		mode->draw();

	flushimage(display, 1);
}

int
edit_cancel(char *buf, int *pos, int maxlen)
{
	USED(maxlen);
	buf[0] = '\0';
	*pos = 0;
	return -1;
}

int
edit_backspace(char *buf, int *pos, int maxlen)
{
	USED(maxlen);
	if(*pos > 0) {
		(*pos)--;
		buf[*pos] = '\0';
		sheet.needredraw = 1;
	}
	return 0;
}

int
edit_add_char(char *buf, int *pos, int maxlen)
{
	USED(buf); USED(pos); USED(maxlen);
	return 0;
}

int
edit_finish(char *buf, int *pos, int maxlen)
{
	USED(buf); USED(pos); USED(maxlen);
	return 1;
}

void
cmd_reload_config(void)
{
	load_config(CONFIG_FILE);
	validate_config();
	apply_config();
	sheet.needredraw = 1;
	fprint(2, "Configuration reloaded from %s\n", CONFIG_FILE);
}

void
cmd_cycle_emoji(void)
{
	static int emoji_set = 0;
	int i;

	emoji_set = (emoji_set + 1) % 7; /* +1 each case */

	switch(emoji_set) {
	case 0:
		for(i = 0; i < 4; i++)
			sheet.emoji_frames[i] = rcc_style[i];
		break;
	case 1:
		for(i = 0; i < 4; i++)
			sheet.emoji_frames[i] = kirby_dance[i];
		break;
	case 2:
		for(i = 0; i < 4; i++)
			sheet.emoji_frames[i] = lambda_dance[i];
		break;
	case 3:
		for(i = 0; i < 4; i++)
			sheet.emoji_frames[i] = dancing_guy[i];
		break;
	case 4:
		for(i = 0; i < 4; i++)
			sheet.emoji_frames[i] = happy_faces[i];
		break;
	case 5:
		for(i = 0; i < 4; i++)
			sheet.emoji_frames[i] = cat_faces[i];
		break;
	case 6:
		for(i = 0; i < 4; i++)
			sheet.emoji_frames[i] = shrug_guys[i];
		break;
	}
	sheet.needredraw = 1;
}

void
cmd_quit(void)
{
	int i;
	for(i = 0; i < 6; i++)
		if(colors[i]) freeimage(colors[i]);
	if(gridcolor) freeimage(gridcolor);

	closedisplay(display);
	exits(nil);
}

void
cmd_save(void)
{
	sheet.current_mode = 3;
	sheet.entering_filename = 1;
	sheet.save_mode = 1;
	strcpy(sheet.filenamebuf, config.default_save_path);
	sheet.filenamepos = strlen(sheet.filenamebuf);
	sheet.needredraw = 1;
}

void
cmd_save_as(void)
{
	sheet.current_mode = 3;
	sheet.entering_filename = 1;
	sheet.save_mode = 1;
	sheet.filenamebuf[0] = '\0';
	sheet.filenamepos = 0;
	sheet.needredraw = 1;
}

void
cmd_open(void)
{
	sheet.current_mode = 3;
	sheet.entering_filename = 1;
	sheet.save_mode = 2;
	strcpy(sheet.filenamebuf, config.default_save_path);
	sheet.filenamepos = strlen(sheet.filenamebuf);
	sheet.needredraw = 1;
}

void
cmd_open_file(void)
{
	sheet.current_mode = 3;
	sheet.entering_filename = 1;
	sheet.save_mode = 2;
	sheet.filenamebuf[0] = '\0';
	sheet.filenamepos = 0;
	sheet.needredraw = 1;
}

void
cmd_start_label(void)
{
	if(sheet.selected >= 0) {
		sheet.current_mode = 2;
		sheet.editing_label = sheet.selected;
		strncpy(sheet.labelbuf, sheet.boxes[sheet.selected].label, 31);
		sheet.labelbuf[31] = '\0';
		sheet.labelpos = strlen(sheet.labelbuf);
		sheet.needredraw = 1;
	}
}

void
cmd_toggle_grid(void)
{
	sheet.gridsnap = !sheet.gridsnap;
	config.gridsnap = sheet.gridsnap;
	sheet.needredraw = 1;
}

void
cmd_delete_box(void)
{
	if(sheet.selected >= 0) {
		delbox(sheet.selected);
		sheet.selected = -1;
		sheet.needredraw = 1;
	}
}

void
cmd_toggle_emoji(void)
{
	sheet.emoji_enabled = !sheet.emoji_enabled;
	config.emoji_enabled = sheet.emoji_enabled;
	sheet.needredraw = 1;
}

void
handlekey(int key)
{
	InputMode *mode = &input_modes[sheet.current_mode];
	if(mode->handler)
		mode->handler(key);
}

void
handlemouse(Mouse m)
{
	InputMode *mode = &input_modes[sheet.current_mode];
	if(mode->mouse_handler)
		mode->mouse_handler(m);
}

void
handle_normal_mode(int key)
{
	Command *cmd;

	for(cmd = commands; cmd->action; cmd++) {
		if(cmd->key == key) {
			cmd->action();
			return;
		}
	}
}

void
handle_cell_edit(int key)
{
	Box *b = &sheet.boxes[sheet.editing];

	if(key == '\n') {
		strcpy(b->formula, sheet.editbuf);

		if(sheet.editbuf[0] == '=') {
			b->type = T_FORMULA;
		} else if(sheet.editbuf[0] == '"') {
			b->type = T_PARAGRAPH;
			/* Add this line to resize the box */
			b->r = Rect(b->pos.x, b->pos.y, b->pos.x + config.paragraph_width, b->pos.y + config.paragraph_height);
		} else {
			char *endp;
			strtod(sheet.editbuf, &endp);
			b->type = (*endp == '\0') ? T_NUMBER : T_TEXT;
		}
		if (b->type >= 0 && b->type < MAXBOXTYPES) {
			BoxType *bt = &boxtypes[b->type];
			if (bt->parse) bt->parse(b);
			if (bt->eval) bt->eval(b);
		}
		recalc_all();

		sheet.editing = -1;
		sheet.current_mode = 0;
		sheet.needredraw = 1;
	} else if(key == Kesc) {
		sheet.editing = -1;
		sheet.current_mode = 0;
		sheet.needredraw = 1;
	} else if(key == Kbs) {
		if(sheet.editpos > 0) {
			sheet.editpos--;
			sheet.editbuf[sheet.editpos] = '\0';
			sheet.needredraw = 1;
		}
	} else if(key >= 32 && key < 127 && sheet.editpos < MAXCONTENT-1) {
		sheet.editbuf[sheet.editpos++] = key;
		sheet.editbuf[sheet.editpos] = '\0';
		sheet.needredraw = 1;
	}
}

void
handle_label_edit(int key)
{
	Box *b = &sheet.boxes[sheet.editing_label];

	if(key == '\n') {
		strncpy(b->label, sheet.labelbuf, 31);
		b->label[31] = '\0';
		sheet.editing_label = -1;
		sheet.current_mode = 0;
		sheet.needredraw = 1;
	} else if(key == Kesc) {
		sheet.editing_label = -1;
		sheet.current_mode = 0;
		sheet.needredraw = 1;
	} else if(key == Kbs) {
		if(sheet.labelpos > 0) {
			sheet.labelpos--;
			sheet.labelbuf[sheet.labelpos] = '\0';
			sheet.needredraw = 1;
		}
	} else if(key >= 32 && key < 127 && sheet.labelpos < 30) {
		sheet.labelbuf[sheet.labelpos++] = key;
		sheet.labelbuf[sheet.labelpos] = '\0';
		sheet.needredraw = 1;
	}
}

void
handle_filename_input(int key)
{
	if(key == '\n') {
		if(sheet.filenamebuf[0] == '\0') {
			strcpy(sheet.filenamebuf, config.default_save_path);
		}

		if(sheet.save_mode == 1) {
			save_spr(sheet.filenamebuf);
		} else {
			load_spr(sheet.filenamebuf);
			recalc_all();
		}

		sheet.entering_filename = 0;
		sheet.current_mode = 0;
		sheet.needredraw = 1;
	} else if(key == Kesc) {
		sheet.entering_filename = 0;
		sheet.current_mode = 0;
		sheet.needredraw = 1;
	} else if(key == '\t') {
		if(!strstr(sheet.filenamebuf, "/tmp/")) {
			strcat(sheet.filenamebuf, "/tmp/");
			sheet.filenamepos = strlen(sheet.filenamebuf);
			sheet.needredraw = 1;
		}
	} else if(key == Kbs) {
		if(sheet.filenamepos > 0) {
			sheet.filenamepos--;
			sheet.filenamebuf[sheet.filenamepos] = '\0';
			sheet.needredraw = 1;
		}
	} else if(key >= 32 && key < 127 && sheet.filenamepos < 250) {
		sheet.filenamebuf[sheet.filenamepos++] = key;
		sheet.filenamebuf[sheet.filenamepos] = '\0';
		sheet.needredraw = 1;
	}
}

void
handle_normal_mouse(Mouse m)
{
	int i;

	if(m.buttons & 1){
		i = boxat(m.xy);
		if(i >= 0){
			sheet.selected = i;

			while(m.buttons & 1){
				sheet.boxes[i].pos = subpt(m.xy, Pt(BOXWIDTH/2, BOXHEIGHT/2));
				if(sheet.gridsnap){
					sheet.boxes[i].pos.x = (sheet.boxes[i].pos.x / sheet.gridsize) * sheet.gridsize;
					sheet.boxes[i].pos.y = (sheet.boxes[i].pos.y / sheet.gridsize) * sheet.gridsize;
				}
				sheet.boxes[i].r = Rect(sheet.boxes[i].pos.x, sheet.boxes[i].pos.y,
					sheet.boxes[i].pos.x + BOXWIDTH, sheet.boxes[i].pos.y + BOXHEIGHT);
				redraw();
				m = emouse();
			}
		} else {
			i = addbox(m.xy);
			if(i >= 0){
				sheet.selected = i;
				sheet.editing = i;
				sheet.current_mode = 1;
				sheet.editbuf[0] = '\0';
				sheet.editpos = 0;
			}
		}
		sheet.needredraw = 1;
	}

	if(m.buttons & 2){
		i = boxat(m.xy);
		if(i >= 0){
			sheet.selected = i;
			sheet.editing = i;
			sheet.current_mode = 1;
			strcpy(sheet.editbuf, sheet.boxes[i].formula);
			sheet.editpos = strlen(sheet.editbuf);
			sheet.needredraw = 1;
		}
	}
}

void
handle_edit_mouse(Mouse m)
{
	if(m.buttons & 4){
		Box *b = &sheet.boxes[sheet.editing];

		strcpy(b->formula, sheet.editbuf);

		if(sheet.editbuf[0] == '=') {
			b->type = T_FORMULA;
		} else {
			char *endp;
			strtod(sheet.editbuf, &endp);
			b->type = (*endp == '\0') ? T_NUMBER : T_TEXT;
		}

		if (b->type >= 0 && b->type < MAXBOXTYPES) {
			BoxType *bt = &boxtypes[b->type];
			if (bt->parse) bt->parse(b);
			if (bt->eval) bt->eval(b);
		}
		recalc_all();

		sheet.editing = -1;
		sheet.current_mode = 0;
		sheet.needredraw = 1;
	}

	int i = boxat(m.xy);
	if(i != sheet.editing && (m.buttons & 1)){
		sheet.editing = -1;
		sheet.current_mode = 0;
		sheet.needredraw = 1;
	}
}

void
handle_label_mouse(Mouse m)
{
	int i = boxat(m.xy);
	if(i != sheet.editing_label && (m.buttons & 1)){
		sheet.editing_label = -1;
		sheet.current_mode = 0;
		sheet.needredraw = 1;
	}
}

void
handle_filename_mouse(Mouse m)
{
	USED(m);
}

void
ctl_addbox(char **args, int nargs)
{
	Point p = Pt(atoi(args[0]), atoi(args[1]));
	int idx = addbox(p);

	if(idx >= 0 && nargs > 2) {
		strncpy(sheet.boxes[idx].formula, args[2], MAXFORMULA-1);
		sheet.boxes[idx].formula[MAXFORMULA-1] = '\0';

		BoxType *bt = &boxtypes[sheet.boxes[idx].type];
		if(bt->parse) bt->parse(&sheet.boxes[idx]);
		if(bt->eval) bt->eval(&sheet.boxes[idx]);

		recalc_all();
	}

	if(idx >= 0 && nargs > 3) {
		strncpy(sheet.boxes[idx].label, args[3], 31);
		sheet.boxes[idx].label[31] = '\0';
	}

	sheet.needredraw = 1;
}

void
ctl_load(char **args, int nargs)
{
	if(nargs >= 1) {
		load_spr(args[0]);
		recalc_all();
		sheet.needredraw = 1;
	}
}

void
ctl_save(char **args, int nargs)
{
	if(nargs >= 1) {
		save_spr(args[0]);
	}
}

void
ctl_quit(char **args, int nargs)
{
	USED(args); USED(nargs);
	cmd_quit();
}

void
check_ctl_file(void)
{
	int fd, nt;

	fd = open(config.ctl_file, OREAD);
	if(fd < 0) {
		return;
	}

	Biobuf bin;
	Binit(&bin, fd, OREAD);
	char *line;

	while((line = Brdline(&bin, '\n'))) {
		line[Blinelen(&bin)-1] = '\0';

		char *tokens[10];
		nt = tokenize(line, tokens, nelem(tokens));
		if(nt == 0) continue;

		CommandHandler *h;
		for(h = cmd_handlers; h->name; h++) {
			if(strcmp(tokens[0], h->name) == 0) {
				if(nt-1 < h->minargs) {
					fprint(2, "%s: needs %d arguments\n",
						h->name, h->minargs);
				} else {
					h->execute(&tokens[1], nt-1);
				}
				break;
			}
		}
	}

	Bterm(&bin);
	close(fd);

	fd = create(config.ctl_file, OWRITE, 0644);
	if(fd >= 0) close(fd);
}

void
eresized(int new)
{
	if(new && getwindow(display, Refnone) < 0)
		sysfatal("can't reattach to window");
}

void
main(int argc, char *argv[])
{
	Event e;
	char *configfile = CONFIG_FILE;

	if(argc > 1)
		configfile = argv[1];

	load_config(configfile);
	validate_config();

	if(access(configfile, AEXIST) < 0) {
		save_config(configfile);
		fprint(2, "Created default configuration at %s\n", configfile);
	}

	if(initdraw(nil, nil, "freebox") < 0)
		sysfatal("initdraw: %r");

	initcolors();

	int ticks = 0;

	einit(Emouse | Ekeyboard);
	eresized(0);

	int ctlfd = create(config.ctl_file, OWRITE, 0644);
	if(ctlfd >= 0) close(ctlfd);

	memset(&sheet, 0, sizeof(sheet));
	sheet.selected = -1;
	sheet.editing = -1;
	sheet.editing_label = -1;
	sheet.entering_filename = 0;
	sheet.current_mode = 0;
	sheet.gridsize = config.gridsize;
	sheet.gridsnap = config.gridsnap;
	init_emoji();
	redraw();

	for(;;){
		switch(event(&e)){
		case Emouse:
			handlemouse(e.mouse);
			break;

		case Ekeyboard:
			handlekey(e.kbdc);
			break;
		}

		ticks++;
		if(ticks % config.ctl_check_interval == 0) {
			check_ctl_file();
		}
		if(ticks % config.redraw_interval == 0) {
			sheet.needredraw = 1;
		}

		if(sheet.needredraw){
			redraw();
			sheet.needredraw = 0;
		}
	}
}