ref: 672db14015b4c9bea798368eb5747dbdbd665da4
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
#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 %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_spr(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
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, "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)
{
/* Display dimensions */
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;
/* Box layout */
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;
/* Dialog dimensions */
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;
/* Paragraph box dimensions */
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;
/* Grid settings */
if(config.gridsize < 8) config.gridsize = 8;
if(config.gridsize > 256) config.gridsize = 256;
/* Emoji settings */
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;
/* Timing intervals */
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;
/* Formula settings */
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;
/* File paths */
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);
/* Boolean values (ensure they're 0 or 1) */
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
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 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
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
eval_paragraph(Box *b)
{
USED(b);
}
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
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;
}
}
}