shithub: fc

Download patch

ref: c5d556434652b8f00fb0ec89fe2d1fc162f5e4c5
author: glenda <glenda@krsna>
date: Sat Aug 16 15:48:47 EDT 2025

ry-again

--- /dev/null
+++ b/fc.c
@@ -1,0 +1,1897 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <keyboard.h>
+#include <bio.h>
+#include <ctype.h>
+
+enum {
+	MAXBOXES = 300,
+	BOXWIDTH = 128,
+	BOXHEIGHT = 32,
+	MAXFORMULA = 256,
+	MAXCONTENT = 256,
+};
+
+enum {
+	T_TEXT = 0,
+	T_NUMBER,
+	T_FORMULA,
+	T_LABEL,
+	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 = '[',  /* Using [ for <= */
+	OP_GE = ']',  /* Using ] for >= */
+	
+	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 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;
+};
+
+struct {
+	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;		/* 1=save, 2=load */
+	int		current_mode;
+	Point	offset;	
+	int		needredraw;
+	int		gridsnap;
+	int		gridsize;
+} sheet;
+
+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;			/* box index */
+		struct {
+			int start, end;	/* range of boxes */
+		} 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); /* For data-oriented mouse handling */
+
+typedef struct InputMode InputMode;
+struct InputMode {
+	char		*name;
+	KeyHandler	handler;
+	MouseHandler mouse_handler; /* Added 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);  /* Returns 1 to exit mode */
+};
+
+typedef struct DrawStep DrawStep;
+struct DrawStep {
+	void (*draw)(void);
+	int  condition;  /* 0=always, 1=if grid, etc */
+};
+
+Image	*colors[6];
+Image	*boxbg;
+Image	*boxselected;
+Image	*boxediting;
+Image	*gridcolor;
+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 draw_box_generic(Box*, Image*);
+void recalc_all(void);
+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_normal_overlay(void);
+void draw_cell_edit_overlay(void);
+void draw_label_edit_overlay(void);
+void draw_filename_overlay(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 draw_background(void);
+void draw_grid_lines(void);
+void draw_all_boxes(void);
+void draw_status_line(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);
+
+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},
+};
+
+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,    "S:save  O:open  L:label  G:grid"},
+	[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},
+	{0, nil}
+};
+
+EditAction edit_actions[] = {
+	{'\n',	edit_finish},
+	{Kesc,	edit_cancel},
+	{Kbs,	edit_backspace},
+	{-1,	edit_add_char},  /* Default for any other key */
+	{0, nil}
+};
+
+DrawStep draw_steps[] = {
+	{draw_background,	0},  /* Always */
+	{draw_grid_lines,	1},  /* If gridsnap */
+	{draw_all_boxes,	0},  /* Always */
+	{draw_status_line,	0},  /* Always */
+	{nil, 0}
+};
+
+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
+initcolors(void)
+{
+	colors[0] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0x000000FF);
+	colors[1] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0xFFFFFFFF);
+	colors[2] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0xEEEEEEFF);
+	colors[3] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0x4444FFFF);
+	colors[4] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0xFF4444FF);
+	colors[5] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0xCCCC88FF);
+	
+	boxbg = colors[1];
+	boxselected = colors[3];
+	boxediting = colors[5];
+	gridcolor = allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0xCCCCCCFF);
+}
+
+double
+round(double x)
+{
+	if(x >= 0)
+		return floor(x + 0.5);
+	else
+		return ceil(x - 0.5);
+}
+
+/* Helper for case-insensitive string comparison */
+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;
+	
+	/* First, check if any box has this exact label */
+	for(i = 0; i < sheet.nboxes; i++) {
+		if(sheet.boxes[i].label[0] && 
+		   cistrcmp(sheet.boxes[i].label, ref) == 0) {
+			return i;
+		}
+	}
+	
+	/* If no labeled box found, try index-based (A=0, B=1, etc.) */
+	/* Handle single letters first */
+	if(strlen(ref) == 1 && isalpha(ref[0])) {
+		int idx = toupper(ref[0]) - 'A';
+		if(idx >= 0 && idx < sheet.nboxes)
+			return idx;
+	}
+	
+	/* Handle double letters (AA, AB, etc.) */
+	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;
+	}
+	
+	/* Try traditional Excel-style (A1, B2) - just use the letter part */
+	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;	/* not found */
+}
+
+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++;	/* skip : */
+				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) {
+				/* It's a cell reference */
+				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++;
+			
+			/* Check for two-char operators */
+			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;	/* circular ref */
+			}
+			
+			if(b->type == T_FORMULA && e->depth < 10) {
+				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;
+	}
+}
+
+/* Evaluate primary expression (number, cell, function call, parentheses) */
+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++;	/* skip function name */
+		
+		/* Expect ( */
+		if(e->pos >= e->ntokens || e->tokens[e->pos].type != TOK_LPAREN)
+			return 0.0;
+		e->pos++;	/* skip ( */
+		
+		/* Collect arguments */
+		Token args[100];
+		int nargs = 0;
+		
+		while(e->pos < e->ntokens && e->tokens[e->pos].type != TOK_RPAREN) {
+			if(nargs > 0) {
+				/* Expect comma */
+				if(e->tokens[e->pos].type != TOK_COMMA)
+					break;
+				e->pos++;	/* skip comma */
+			}
+			
+			/* Check for range */
+			if(e->tokens[e->pos].type == TOK_RANGE) {
+				/* Expand range into individual cells */
+				int start = e->tokens[e->pos].range.start;
+				int end = e->tokens[e->pos].range.end;
+				e->pos++;
+				
+				/* Add all cells in range */
+				int i;
+				for(i = start; i <= end && i >= 0 && nargs < 100; i++) {
+					args[nargs].type = TOK_CELL;
+					args[nargs].cell = i;
+					nargs++;
+				}
+			} else {
+				/* Regular argument - evaluate it */
+				args[nargs].type = TOK_NUM;
+				args[nargs].num = eval_expr(e);
+				nargs++;
+			}
+		}
+		
+		/* Skip ) */
+		if(e->pos < e->ntokens && e->tokens[e->pos].type == TOK_RPAREN)
+			e->pos++;
+		
+		/* Call function */
+		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++;	/* skip ( */
+		result = eval_expr(e);
+		/* Skip ) */
+		if(e->pos < e->ntokens && e->tokens[e->pos].type == TOK_RPAREN)
+			e->pos++;
+		break;
+		
+	case TOK_OP:
+		/* Unary minus */
+		if(t->op == OP_SUB) {
+			e->pos++;
+			result = -eval_primary(e);
+		}
+		break;
+	}
+	
+	return result;
+}
+
+/* Evaluate factor (handle power) */
+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++;	/* skip operator */
+		double right = eval_primary(e);
+		left = pow(left, right);
+	}
+	
+	return left;
+}
+
+/* Evaluate term (multiplication, division, modulo) */
+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;	/* div by zero */
+			}
+			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;
+}
+
+/* Evaluate expression (addition, subtraction, comparison) */
+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;	/* large number */
+	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;	/* small number */
+	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++;	/* direct number */
+		}
+	}
+	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 {
+		/* Round to N decimal places */
+		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)
+{
+	/* LOOKUP(value, search_range, result_range) */
+	/* Simplified VLOOKUP - finds value in search range, returns corresponding from result range */
+	if(nargs < 2)
+		return 0.0;
+	
+	/* For now, just return the value */
+	/* Full implementation would search through cells */
+	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, "%.2f", val);
+		} else {
+			b->type = T_TEXT;
+			strncpy(b->content, b->formula, MAXCONTENT);
+		}
+		return;
+	}
+	
+	/* It's a formula */
+	b->type = T_FORMULA;
+	
+	/* Tokenize */
+	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;
+	
+	/* Extract cell references for dependency tracking */
+	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) {
+			/* Add range endpoints */
+			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] == '"') {
+		/* String formula - show as text */
+		strncpy(b->content, b->formula + 2, MAXCONTENT);
+		char *p = strchr(b->content, '"');
+		if(p) *p = '\0';
+	} else {
+		snprint(b->content, MAXCONTENT, "%.2f", b->value);
+	}
+	
+	b->dirty = 0;
+	
+	/* Mark dependent cells as dirty */
+	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;
+				}
+			}
+		}
+	}
+}
+
+/* Recalculate all formulas in dependency order */
+void
+recalc_all(void)
+{
+	int i, changed;
+	int passes = 0;
+	
+	/* Mark all formulas as dirty */
+	for(i = 0; i < sheet.nboxes; i++) {
+		if(sheet.boxes[i].type == T_FORMULA)
+			sheet.boxes[i].dirty = 1;
+	}
+	
+	/* Iteratively evaluate until no changes (or max passes) */
+	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 < 10);
+}
+
+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));
+	
+	/* Snap to grid if enabled */
+	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;
+	
+	/* Shift boxes down */
+	memmove(&sheet.boxes[i], &sheet.boxes[i+1], 
+		(sheet.nboxes - i - 1) * sizeof(Box));
+	sheet.nboxes--;
+	
+	/* Update references in other boxes */
+	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;  /* invalidate */
+		}
+	}
+}
+
+void
+parse_text(Box *b)
+{
+	/* copy content as is */
+	strncpy(b->content, b->formula, MAXCONTENT);
+}
+
+void
+parse_number(Box *b)
+{
+	char *endp;
+	b->value = strtod(b->formula, &endp);
+	snprint(b->content, MAXCONTENT, "%.2f", b->value);
+}
+
+void
+eval_text(Box *b)
+{
+	/* Text doesn't evaluate */
+	USED(b);
+}
+
+void
+eval_number(Box *b)
+{
+	/* Numbers are already evaluated */
+	USED(b);
+}
+
+void
+draw_box_generic(Box *b, Image *dst)
+{
+	Image *bg = boxbg;
+	int idx = b - sheet.boxes;  /* Get box index */
+	
+	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]) {
+		/* Has custom label */
+		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 {
+		/* Show default index (A, B, C...) */
+		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(5, 16));  /* Offset down to avoid label */
+	
+	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){
+		string(dst, Pt(b->r.max.x - 10, b->r.min.y + 2), colors[3], ZP, font, "=");
+	}
+}
+
+void
+drawgrid(Image *dst)
+{
+    int x, y;
+    Rectangle r = screen->r;
+    
+    // Start from the first grid line that's visible in the window
+    int startx = (r.min.x / sheet.gridsize) * sheet.gridsize;
+    int starty = (r.min.y / sheet.gridsize) * sheet.gridsize;
+    
+    // Draw vertical lines
+    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);
+    }
+    
+    // Draw horizontal lines
+    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)
+{
+	/* Nothing special for normal mode */
+}
+
+void
+draw_cell_edit_overlay(void)
+{
+	/* Could draw edit indicator */
+}
+
+void
+draw_label_edit_overlay(void)
+{
+	/* Could highlight the box being labeled */
+}
+
+void
+draw_filename_overlay(void)
+{
+	Rectangle r;
+	int w = 256;
+	int h = 48;
+	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 dialog */
+	draw(screen, r, colors[1], nil, ZP);
+	border(screen, r, 2, colors[0], ZP);
+	
+	/* Title */
+	char *title = sheet.save_mode == 1 ? "Save As:" : "Open File:";
+	string(screen, Pt(r.min.x + 10, r.min.y + 5), 
+		colors[0], ZP, font, title);
+	
+	/* Filename with cursor */
+	char display[256];
+	snprint(display, sizeof(display), "%s_", sheet.filenamebuf);
+	string(screen, Pt(r.min.x + 10, 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 + 10, screen->r.max.y - 20), 
+			colors[0], ZP, font, buf);
+}
+
+void
+redraw(void)
+{
+	DrawStep *step;
+	
+	/* Execute drawing steps from table */
+	for(step = draw_steps; step->draw; step++) {
+		if(step->condition == 0 || 
+		   (step->condition == 1 && sheet.gridsnap)) {
+			step->draw();
+		}
+	}
+	
+	/* Draw mode-specific overlay */
+	InputMode *mode = &input_modes[sheet.current_mode];
+	if(mode->draw)
+		mode->draw();
+	
+	flushimage(display, 1);
+}
+
+int
+edit_finish(char *buf, int *pos, int maxlen)
+{
+	USED(buf); USED(pos); USED(maxlen);
+	return 1;  /* Signal to exit edit mode */
+}
+
+int
+edit_cancel(char *buf, int *pos, int maxlen)
+{
+	USED(maxlen);
+	buf[0] = '\0';
+	*pos = 0;
+	return -1;  /* Signal cancellation */
+}
+
+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)
+{
+	/* Note: key is passed through global or parameter */
+	/* For this example, we'll use a global current_key */
+	return 0;
+}
+
+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;  /* Enter filename mode */
+	sheet.entering_filename = 1;
+	sheet.save_mode = 1;
+	strcpy(sheet.filenamebuf, "/tmp/sheet.spr");
+	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, "/tmp/sheet.spr");
+	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;  /* Label edit mode */
+		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;
+	sheet.needredraw = 1;
+}
+
+void
+cmd_delete_box(void)
+{
+	if(sheet.selected >= 0) {
+		delbox(sheet.selected);
+		sheet.selected = -1;
+		sheet.needredraw = 1;
+	}
+}
+
+void
+handle_normal_mode(int key)
+{
+	Command *cmd;
+	
+	/* Look up command in table */
+	for(cmd = commands; cmd->action; cmd++) {
+		if(cmd->key == key) {
+			cmd->action();
+			return;
+		}
+	}
+	
+	/* No match - could show help or ignore */
+}
+
+void
+handle_cell_edit(int key)
+{
+	Box *b = &sheet.boxes[sheet.editing];
+	
+	if(key == '\n') {
+		/* Save the edit */
+		strcpy(b->formula, sheet.editbuf);
+		
+		/* Determine type */
+		if(sheet.editbuf[0] == '=') {
+			b->type = T_FORMULA;
+		} else {
+			char *endp;
+			strtod(sheet.editbuf, &endp);
+			b->type = (*endp == '\0') ? T_NUMBER : T_TEXT;
+		}
+		
+		/* Parse and evaluate */
+		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();
+		
+		/* Return to normal mode */
+		sheet.editing = -1;
+		sheet.current_mode = 0;
+		sheet.needredraw = 1;
+	} else if(key == Kesc) {
+		/* Cancel */
+		sheet.editing = -1;
+		sheet.current_mode = 0;
+		sheet.needredraw = 1;
+	} else if(key == Kbs) {
+		/* Backspace */
+		if(sheet.editpos > 0) {
+			sheet.editpos--;
+			sheet.editbuf[sheet.editpos] = '\0';
+			sheet.needredraw = 1;
+		}
+	} else if(key >= 32 && key < 127 && sheet.editpos < MAXCONTENT-1) {
+		/* Add character */
+		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') {
+		/* Save label */
+		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) {
+		/* Cancel */
+		sheet.editing_label = -1;
+		sheet.current_mode = 0;
+		sheet.needredraw = 1;
+	} else if(key == Kbs) {
+		/* Backspace */
+		if(sheet.labelpos > 0) {
+			sheet.labelpos--;
+			sheet.labelbuf[sheet.labelpos] = '\0';
+			sheet.needredraw = 1;
+		}
+	} else if(key >= 32 && key < 127 && sheet.labelpos < 30) {
+		/* Add character */
+		sheet.labelbuf[sheet.labelpos++] = key;
+		sheet.labelbuf[sheet.labelpos] = '\0';
+		sheet.needredraw = 1;
+	}
+}
+
+void
+save(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 %s\n", box->formula);
+		Bprint(b, "  value %g\n", box->value);
+		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(char *file)
+{
+	Biobuf *b;
+	char *line;
+	char *fields[10];
+	int nf;
+	
+	b = Bopen(file, OREAD);
+	if(b == nil){
+		fprint(2, "cannot open %s: %r\n", file);
+		return;
+	}
+	
+	sheet.nboxes = 0;
+	
+	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 && strcmp(fields[0], "box") == 0){
+			if(sheet.nboxes < MAXBOXES)
+				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) {
+				sheet.boxes[sheet.nboxes-1].type = type;
+			} else {
+				/* Default to text type if file is corrupted */
+				sheet.boxes[sheet.nboxes-1].type = T_TEXT;
+			}
+		} else if(nf >= 2 && strcmp(fields[0], "formula") == 0){
+			strcpy(sheet.boxes[sheet.nboxes-1].formula, fields[1]);
+		} else if(nf >= 2 && strcmp(fields[0], "value") == 0){
+			sheet.boxes[sheet.nboxes-1].value = atof(fields[1]);
+		} else if(nf >= 2 && strcmp(fields[0], "label") == 0){
+			strncpy(sheet.boxes[sheet.nboxes-1].label, fields[1], 31);
+		}
+	}
+	
+	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);
+		}
+	}
+	redraw();
+}
+
+void
+handle_filename_input(int key)
+{
+	if(key == '\n') {
+		/* Execute save/load */
+		if(sheet.filenamebuf[0] == '\0') {
+			strcpy(sheet.filenamebuf, "/tmp/sheet.spr");
+		}
+		
+		if(sheet.save_mode == 1) {
+			save(sheet.filenamebuf);
+		} else {
+			load(sheet.filenamebuf);
+			recalc_all();
+		}
+		
+		sheet.entering_filename = 0;
+		sheet.current_mode = 0;
+		sheet.needredraw = 1;
+	} else if(key == Kesc) {
+		/* Cancel */
+		sheet.entering_filename = 0;
+		sheet.current_mode = 0;
+		sheet.needredraw = 1;
+	} else if(key == '\t') {
+		/* Tab completion */
+		if(!strstr(sheet.filenamebuf, ".spr")) {
+			strcat(sheet.filenamebuf, ".spr");
+			sheet.filenamepos = strlen(sheet.filenamebuf);
+			sheet.needredraw = 1;
+		}
+	} else if(key == Kbs) {
+		/* Backspace */
+		if(sheet.filenamepos > 0) {
+			sheet.filenamepos--;
+			sheet.filenamebuf[sheet.filenamepos] = '\0';
+			sheet.needredraw = 1;
+		}
+	} else if(key >= 32 && key < 127 && sheet.filenamepos < 250) {
+		/* Add character */
+		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){
+		/* Left click */
+		i = boxat(m.xy);
+		if(i >= 0){
+			/* Select existing box */
+			sheet.selected = i;
+			
+			/* Drag box with button held */
+			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 {
+			/* Create new box */
+			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){
+		/* Middle click - edit box */
+		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];
+		
+		/* Save the edit */
+		strcpy(b->formula, sheet.editbuf);
+		
+		/* Determine type */
+		if(sheet.editbuf[0] == '=') {
+			b->type = T_FORMULA;
+		} else {
+			char *endp;
+			strtod(sheet.editbuf, &endp);
+			b->type = (*endp == '\0') ? T_NUMBER : T_TEXT;
+		}
+		
+		/* Parse and evaluate */
+		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();
+		
+		/* Return to normal mode */
+		sheet.editing = -1;
+		sheet.current_mode = 0;
+		sheet.needredraw = 1;
+	}
+
+	/* Clicking outside the box could cancel the edit */
+	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)
+{
+	/* Clicking outside the box could cancel the label edit */
+	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)
+{
+	/* Clicks do nothing in this mode */
+	USED(m);
+}
+
+
+void 
+eresized(int new) 
+{
+	if(new && getwindow(display, Refnone) < 0)
+		sysfatal("can't reattach to window");
+		
+}
+
+void
+main(int argc, char *argv[])
+{
+	Event e;
+	
+	USED(argc);
+	USED(argv);
+	
+	if(initdraw(nil, nil, "freecell") < 0)
+		sysfatal("initdraw: %r");
+	
+	initcolors();
+	einit(Emouse | Ekeyboard);
+	eresized(0);
+
+	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 = 32;
+	sheet.gridsnap = 1;
+	
+	redraw();
+	
+	for(;;){
+		switch(event(&e)){
+		case Emouse:
+			handlemouse(e.mouse);
+			break;
+			
+		case Ekeyboard:
+			handlekey(e.kbdc);
+			break;
+		}
+		
+		if(sheet.needredraw){
+			redraw();
+			sheet.needredraw = 0;
+		}
+	}
+}
--- /dev/null
+++ b/mkfile
@@ -1,0 +1,24 @@
+
+</$objtype/mkfile
+
+# Compiler flags
+CFLAGS=-FVTw
+
+# Targets
+BIN=$home/bin/amd64
+TARG=fc
+
+# Source files
+GAME=fc.6
+
+# Build rules
+all:V: $BIN/$TARG
+
+$GAME: $TARG.c
+	6c $CFLAGS $TARG.c
+
+$BIN/$TARG: $GAME
+	6l -o $target $GAME
+
+nuke:
+	rm -f *.6 $BIN/$TARG 
--