ref: f0fae9432d1f553165d7d03412055b7e41ad57fd
dir: /fc.c/
#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 /* 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, 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 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); }; Config config; Sheet sheet; Image *colors[6]; Image *boxbg; Image *boxselected; Image *boxediting; Image *gridcolor; void load_config(char *path); void save_config(char *path); void apply_config(void); 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 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_box_generic(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}, {"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} }; 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, "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_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, "# FreeBox Configuration File\n"); Bprint(b, "# Generated automatically - edit to customize\n"); Bprint(b, "# Reload with 'r' key while running\n\n"); Bprint(b, "# Visual Settings\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, "\n# Dialog Settings\n"); 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, "\n# Animation Settings\n"); 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, "\n# Grid Settings\n"); Bprint(b, "gridsize %d\n", config.gridsize); Bprint(b, "gridsnap %d\n", config.gridsnap); Bprint(b, "\n# Behavior Settings\n"); 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, "\n# Paths\n"); Bprint(b, "ctl_file %s\n", config.ctl_file); Bprint(b, "default_save %s\n", config.default_save_path); Bprint(b, "\n# Colors (RGBA hex)\n"); 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.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.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.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); 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 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 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, 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); } 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_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 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 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_finish(char *buf, int *pos, int maxlen) { USED(buf); USED(pos); USED(maxlen); return 1; } 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]; } 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 cmd_cycle_emoji(void) { static int emoji_set = 0; int i; emoji_set = (emoji_set + 1) % 7; 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_toggle_emoji(void) { sheet.emoji_enabled = !sheet.emoji_enabled; config.emoji_enabled = sheet.emoji_enabled; sheet.needredraw = 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; } 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 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 { 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 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; } 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; 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) { sheet.boxes[sheet.nboxes-1].type = type; } else { 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); } } recalc_all(); redraw(); } 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(sheet.filenamebuf); } else { load(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(args[0]); recalc_all(); sheet.needredraw = 1; } } void ctl_save(char **args, int nargs) { if(nargs >= 1) { save(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; } } }