shithub: da

ref: 091851703eba8be72f145fdf961cad0e1f250611
dir: /da.c/

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

#define CONFIG_FILE 						"/tmp/da.conf"
#define DEFAULT_CTL_FILE 					"/tmp/da.ctl"
#define DEFAULT_SAVE_PATH 					"/tmp/canvas.da"
#define DEFAULT_CTL_CHECK_INTERVAL 			200
#define DEFAULT_REDRAW_INTERVAL 			30
#define DEFAULT_BANNER_HEIGHT 				25
#define DEFAULT_STATUS_HEIGHT 				25
#define DEFAULT_STATUS_MARGIN 				10
#define DEFAULT_EMOJI_SPEED 				3
#define DEFAULT_EMOJI_FRAME_DELAY 			10
#define DEFAULT_BOXWIDTH 					16
#define DEFAULT_BOXHEIGHT 					16
#define DEFAULT_BIGBOXWIDTH 				24
#define DEFAULT_BIGBOXHEIGHT 				24
#define DEFAULT_GRIDSIZE					16
#define DEFAULT_GRIDSNAP					1
#define DEFAULT_EMOJI						1
/* const */
#define MAXBOXES 							2000


enum {
	T_BOX = 0,
	T_BIGBOX,
	MAXBOXTYPES,
};

typedef struct Config Config;
struct Config {
	int banner_height;
	int status_height;
	int status_margin;
	int box_width;
	int box_height;
	int bigbox_width;
	int bigbox_height;
	int emoji_enabled;
	int emoji_speed;
	int emoji_frame_delay;
	int ctl_check_interval;
	int redraw_interval;
	int gridsnap;
	int gridsize;
	char ctl_file[256];
	char default_save_path[256];
	ulong color_bg;
	ulong color_fg;
	ulong color_selected;
	ulong color_grid;
};

typedef enum {
	CFG_INT,
	CFG_STRING,
	CFG_BOOL,
	CFG_COLOR,

} ConfigType;

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

typedef struct Box Box;
struct Box {
	Point		pos;
	Rectangle	r;
	int			type;
	int			selected;
	ulong 		color;
};

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

typedef struct Canvas Canvas;
struct Canvas {
	Box		boxes[MAXBOXES];
	int		nboxes;
	int		selected;
	int		fnkey;
	char	fnbuf[256];
	int		fnpos;
	int		save_mode;
	int		current_mode;
	Point	offset;
	int		needredraw;
	int		gridsnap;
	int		gridsize;
	int		emoji_pos;
	int		emoji_frame;
	int		emoji_dir; /* direction */
	char	*emoji_frames[4];
	int		emoji_enabled;
	int 	current_color;
	int 	next_box_type;  
};

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 DrawStep DrawStep;
struct DrawStep {
	void (*draw)(void);
	int  condition;
};

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

Image *colors[16];
Image *boxselected;
Image *gridcolor;
Image *cfg;
Image *cbg;
Config config;
Canvas canvas;

void save_da(char *file);
void load_da(char *file);
void redraw(void);
void load_config(char *path);
void save_config(char *path);
void apply_config(void);
void norm_key(int key);
void norm_mouse(Mouse m);
void file_key(int key);
void fn_mouse(Mouse m);
void handlekey(int key);
void handlemouse(Mouse m);
void cleanup(void);
void draw_box(Box*, Image*);
void draw_bigbox(Box*, Image*);
void draw_nol(void);
void draw_fnol(void);
void draw_bg(void);
void draw_grid(void);
void draw_all(void);
void draw_emoji_banner(void);
void draw_status(void);
void cmd_set_color(int idx);
void cmd_quit(void);
void cmd_save(void);
void cmd_save_as(void);
void cmd_open(void);
void cmd_open_file(void);
void cmd_toggle_grid(void);
void cmd_toggle_boxtype(void);
void cmd_toggle_emoji(void);
void cmd_delete_box(void);
void cmd_cycle_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_config_defaults(void);
void init_emoji(void);
void initcolors(void);
void validate_config(void);
void apply_config(void);

static ulong palette[] = {
	0x2ECC71FF, 0x34495EFF, 0x95A5A6FF, 0xE67E22FF,
	0xF39C12FF, 0x3498DBFF, 0x7F8C8DFF, 0x1ABC9CFF,
	0xE74C3CFF, 0x9B59B6FF, 0xF1C40FFF, 0xECF0F1FF,
	0x2C3E50FF, 0x16A085FF, 0xD35400FF, 0x27AE60FF
};

ConfigField config_fields[] = {
	{"banner_height",				CFG_INT,	&config.banner_height,				0},
	{"status_height",				CFG_INT,	&config.status_height,				0},
	{"status_margin",				CFG_INT,	&config.status_margin,				0},
	{"box_width",					CFG_INT,	&config.box_width,					0},
	{"box_height",					CFG_INT,	&config.box_height,					0},
	{"bigbox_width",				CFG_INT,	&config.bigbox_width,				0},
	{"bigbox_height",				CFG_INT,	&config.bigbox_height,				0},
	{"emoji_enabled",				CFG_BOOL,	&config.emoji_enabled,				0},
	{"emoji_speed",					CFG_INT,	&config.emoji_speed,				0},
	{"emoji_frame_delay",			CFG_INT,	&config.emoji_frame_delay,			0},
	{"ctl_check_interval",			CFG_INT,	&config.ctl_check_interval,			0},
	{"redraw_interval",				CFG_INT,	&config.redraw_interval,			0},
	{"gridsize",					CFG_INT,	&config.gridsize,					0},
	{"gridsnap",					CFG_BOOL,	&config.gridsnap,					0},
	{"ctl_file",					CFG_STRING,	config.ctl_file,					256},
	{"default_save",				CFG_STRING,	config.default_save_path,			256},
	{"color_bg",					CFG_COLOR,	&config.color_bg,					0},
	{"color_fg",					CFG_COLOR,	&config.color_fg,					0},
	{"color_selected",				CFG_COLOR,	&config.color_selected,				0},
	{"color_grid",					CFG_COLOR,	&config.color_grid,					0},
	{nil, 0, nil, 0} /* sentinel */
};

BoxType boxtypes[] = {
	[T_BOX]		= {"box", draw_box},
	[T_BIGBOX] 	= {"bigbox", draw_bigbox},
};

InputMode input_modes[] = {
	[0] = {
			"normal",
			norm_key,	
			norm_mouse,	
			draw_nol,	
			"0-F: Colors  H:Emoji  h:cycle  S:ave  O:pen  g:rid  r:eload-cfg "
		},
	[1] = {
			"filename",
			file_key,
			fn_mouse,
			draw_fnol,
			"Tab:/tmp/  Enter:confirm  Esc:cancel"
		},
};

Command commands[] = {
	{Kdel,	cmd_quit},
	{'s',	cmd_save},
	{'S',	cmd_save_as},
	{'o',	cmd_open},
	{'O',	cmd_open_file},
	{'g',	cmd_toggle_grid},
	{'d',	cmd_delete_box},
	{'H',   cmd_toggle_emoji},
	{'h',   cmd_cycle_emoji},
	{'r',   cmd_reload_config},
	{'t', 	cmd_toggle_boxtype}, 
	{0, nil}
};

DrawStep draw_steps[] = {
	{draw_bg,	0},
	{draw_grid,	1},
	{draw_all,	0},
	{draw_status,	0},
	{nil, 0}
};

CtlHandler ctl_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 *lenny[] = {
    "( *_*)",  
    "( o_o)",    
    "( ^_^)",
    "( >_>)"
};

void
save_da(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 < canvas.nboxes; i++){
		box = &canvas.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, "  color %08lux\n", box->color);
	}

	Bterm(b);
	close(fd);
}

void
load_da(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(&canvas, 0, sizeof(canvas));
	canvas.selected = -1;
	canvas.fnkey = 0;
	canvas.current_mode = 0;
	canvas.current_color = 8;
	canvas.gridsize = config.gridsize;
	canvas.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(canvas.nboxes < MAXBOXES){
				Box *box = &canvas.boxes[canvas.nboxes];
				memset(box, 0, sizeof(Box));
				box->r = Rect(0, 0, config.box_width, config.box_height);
				canvas.nboxes++;
			}
		} else if(nf >= 3 && strcmp(fields[0], "pos") == 0){
			Box *box = &canvas.boxes[canvas.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 + config.box_width, box->pos.y + config.box_height);
		} else if(nf >= 2 && strcmp(fields[0], "type") == 0){
			int type = atoi(fields[1]);
			if (type >= 0 && type < MAXBOXTYPES) {
				Box *box = &canvas.boxes[canvas.nboxes-1];
				box->type = type;
				
				if(type == T_BIGBOX){
					box->r = Rect(box->pos.x, box->pos.y, 
						box->pos.x + config.bigbox_width, 
						box->pos.y + config.bigbox_height);
				}
			}
		} else if(nf >= 2 && strcmp(fields[0], "color") == 0){
			canvas.boxes[canvas.nboxes-1].color = strtoul(fields[1], nil, 16);
		}
	}

	Bterm(b);
	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, "# auto-gen da.conf $\n");	
	Bprint(b, "banner_height %d\n", config.banner_height);
	Bprint(b, "status_height %d\n", config.status_height);
	Bprint(b, "status_margin %d\n", config.status_margin);
	Bprint(b, "box_width %d\n", config.box_width);
	Bprint(b, "box_height %d\n", config.box_height);
	Bprint(b, "bigbox_width %d\n", config.bigbox_width);
	Bprint(b, "bigbox_height %d\n", config.bigbox_height);
	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, "gridsnap %d\n", config.gridsnap);
	Bprint(b, "gridsize %d\n", config.gridsize);
	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_selected %08lux\n", config.color_selected);
	Bprint(b, "color_grid %08lux\n", config.color_grid);
	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_width = DEFAULT_BOXWIDTH;
	config.box_height = DEFAULT_BOXHEIGHT;
	config.bigbox_width = DEFAULT_BIGBOXWIDTH;
	config.bigbox_height = DEFAULT_BIGBOXHEIGHT;
	config.emoji_enabled = DEFAULT_EMOJI;
	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 = DEFAULT_GRIDSNAP;
	config.gridsize = DEFAULT_GRIDSIZE;
	config.color_bg = 0xEEEEEEFF;
	config.color_fg = 0x000000FF;
	config.color_selected = 0x4444FFFF;
	config.color_grid = 0xCCCCCCFF;
	strcpy(config.ctl_file, DEFAULT_CTL_FILE);
	strcpy(config.default_save_path, DEFAULT_SAVE_PATH);
}

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;
				}

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

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

	Bterm(b);
}

void
validate_config(void)
{
	if(config.banner_height < 0) config.banner_height = 0;
	if(config.banner_height > 100) config.banner_height = 100;
	
	if(config.status_height < 0) config.status_height = 0;
	if(config.status_height > 100) config.status_height = 100;
	
	if(config.status_margin < 0) config.status_margin = 0;
	if(config.status_margin > 50) config.status_margin = 50;

	if(config.box_width < 1) config.box_width = 1;
	if(config.box_width > 512) config.box_width = 512;
	
	if(config.box_height < 1) config.box_height = 1;
	if(config.box_height > 512) config.box_height = 512;

	if(config.bigbox_width < 2) config.bigbox_width = 2;
	if(config.bigbox_width > 1024) config.bigbox_width = 1024;
	
	if(config.bigbox_height < 2) config.bigbox_height = 2;
	if(config.bigbox_height > 1024) config.bigbox_height = 1024;
	
	if(config.gridsize < 1) config.gridsize = 1;
	if(config.gridsize > 512) config.gridsize = 512;
	
	if(config.emoji_speed < 1) config.emoji_speed = 1;
	if(config.emoji_speed > 20) config.emoji_speed = 20;
	
	if(config.emoji_frame_delay < 1) config.emoji_frame_delay = 1;
	if(config.emoji_frame_delay > 100) config.emoji_frame_delay = 100;
	
	if(config.ctl_check_interval < 10) config.ctl_check_interval = 10;
	if(config.ctl_check_interval > 1000) config.ctl_check_interval = 1000;
	
	if(config.redraw_interval < 10) config.redraw_interval = 10;
	if(config.redraw_interval > 1000) config.redraw_interval = 1000;
	
	if(config.ctl_file[0] == '\0')
		strcpy(config.ctl_file, DEFAULT_CTL_FILE);
	if(config.default_save_path[0] == '\0')
		strcpy(config.default_save_path, DEFAULT_SAVE_PATH);
	
	config.emoji_enabled = config.emoji_enabled ? 1 : 0;
	config.gridsnap = config.gridsnap ? 1 : 0;
	
	/* Note: Color values don't need validation as they're ulong hex values */
	/* Any value is technically valid for a color */
}

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

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

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

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

void
initcolors(void)
{
	int i;
	for(i = 0; i < 16; i++) {
		colors[i] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, palette[i]);
		if(!colors[i])
			sysfatal("allocimage failed");
	}
	cbg = allocimage(display, Rect(0,0,1,1), screen->chan, 1, config.color_bg);
	boxselected = allocimage(display, Rect(0,0,1,1), screen->chan, 1, config.color_selected);
	cfg = allocimage(display, Rect(0,0,1,1), screen->chan, 1, config.color_fg);
	gridcolor = allocimage(display, Rect(0,0,1,1), screen->chan, 1, config.color_grid);
}

void 
cleanup(void)
{
    int i;
    for(i = 0; i < 16; i++) {
        if(colors[i])
            freeimage(colors[i]);
    }
}

void
cmd_set_color(int idx)
{
	if(idx >= 0 && idx < 16) {
		canvas.current_color = idx;
		if(canvas.selected >= 0 && canvas.selected < canvas.nboxes) {
			canvas.boxes[canvas.selected].color = palette[idx];
		}
		canvas.needredraw = 1;
	}
}

void 
cmd_toggle_boxtype(void)
{
    canvas.next_box_type = !canvas.next_box_type;
    canvas.needredraw = 1;
}

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

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

int
addbox(Point p)
{
    Box *b;
    if(canvas.nboxes >= MAXBOXES)
        return -1;

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

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

    b->pos = p;
    b->type = canvas.next_box_type; 
    b->color = palette[canvas.current_color];

    if(b->type == T_BIGBOX) {
        b->r = Rect(p.x, p.y, 
            p.x + config.bigbox_width,   
            p.y + config.bigbox_height);  
    } else {
        b->r = Rect(p.x, p.y, 
            p.x + config.box_width,       
            p.y + config.box_height);      
    }
    
    return canvas.nboxes++;
}

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

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

void
draw_box(Box *b, Image *dst)
{
    Image *bg = colors[8];  // default color
    
    if(b->color != 0) {
        int i;
        for(i = 0; i < 16; i++) {
            if(palette[i] == b->color) {
                bg = colors[i];
                break;
            }
        }
    }
    
    if(b->selected)
        bg = boxselected;
    
    draw(dst, b->r, bg, nil, ZP);
}

void
draw_bigbox(Box *b, Image *dst)
{
    Image *bg = colors[4];
    
    if(b->color != 0) {
        int i;
        for(i = 0; i < 16; i++) {
            if(palette[i] == b->color) {
                bg = colors[i];
                break;
            }
        }
    }
    
    if(b->selected)
        bg = boxselected;
    
    draw(dst, b->r, bg, nil, ZP);
}

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

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

	for(x = startx; x <= r.max.x; x += canvas.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 += canvas.gridsize){
		line(dst, Pt(r.min.x, y), Pt(r.max.x, y), 0, 0, 0, gridcolor, ZP);
	}
}

void
draw_nol(void)
{
}

void
draw_fnol(void)
{
    Rectangle r;
    int w = 400;
    int h = 64;
    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,cbg, nil, ZP); 
    border(screen, r, 2, cfg, ZP);

    char *title = canvas.save_mode == 1 ? "Save As:" : "Open File:";
    string(screen, Pt(r.min.x + 10, r.min.y + 5),
        cfg, ZP, font, title); 

    char display[256];
    snprint(display, sizeof(display), "%s_", canvas.fnbuf);
    string(screen, Pt(r.min.x + 10, r.min.y + 20),
        cfg, ZP, font, display); 
}

void
draw_bg(void)
{
	draw(screen, screen->r, cbg, nil, ZP);
}

void
draw_grid(void)
{
	if(!canvas.gridsnap)
		return;
	drawgrid(screen);
}

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

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

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

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

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

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

	char *emoji = canvas.emoji_frames[canvas.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, cbg, nil, ZP);

	Point pos = Pt(canvas.emoji_pos, screen->r.min.y + 5);
	string(screen, pos, cfg, ZP, font, emoji);

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

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

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

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 && canvas.gridsnap)) {
			step->draw();
		}
	}
	replclipr(screen, 0, screen->r);

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

	flushimage(display, 1);
}

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

void
cmd_cycle_emoji(void)
{
    static int emoji_set = 0;
    int i;
    
    emoji_set = (emoji_set + 1) % 3;  /* 3 total emoji sets */
    
    switch(emoji_set) {
    case 0:
        for(i = 0; i < 4; i++)
            canvas.emoji_frames[i] = lenny[i];
        break;
    case 1:
        for(i = 0; i < 4; i++)
            canvas.emoji_frames[i] = dancing_guy[i];
        break;
    case 2:
        for(i = 0; i < 4; i++)
            canvas.emoji_frames[i] = happy_faces[i];
        break;
 	}
    canvas.needredraw = 1;

}

void
cmd_quit(void)
{
	cleanup();
	exits(nil);
}

void
cmd_save(void)
{
	canvas.current_mode = 1;
	canvas.fnkey = 1;
	canvas.save_mode = 1;
	strcpy(canvas.fnbuf, config.default_save_path);
	canvas.fnpos = strlen(canvas.fnbuf);
	canvas.needredraw = 1;
}

void
cmd_save_as(void)
{
	canvas.current_mode = 1;
	canvas.fnkey = 1;
	canvas.save_mode = 1;
	canvas.fnbuf[0] = '\0';
	canvas.fnpos = 0;
	canvas.needredraw = 1;
}

void
cmd_open(void)
{
	canvas.current_mode = 1;
	canvas.fnkey = 1;
	canvas.save_mode = 2;
	strcpy(canvas.fnbuf, config.default_save_path);
	canvas.fnpos = strlen(canvas.fnbuf);
	canvas.needredraw = 1;
}

void
cmd_open_file(void)
{
	canvas.current_mode = 1;
	canvas.fnkey = 1;
	canvas.save_mode = 2;
	canvas.fnbuf[0] = '\0';
	canvas.fnpos = 0;
	canvas.needredraw = 1;
}

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

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

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

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

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

void
norm_key(int key)
{
	Command *cmd;

	if(key >= '0' && key <= '9') {
		cmd_set_color(key - '0');
		return;
	}
	
	if(key >= 'a' && key <= 'f') {
		cmd_set_color(10 + (key - 'a'));
		return;
	}

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

void
file_key(int key)
{
	if(key == '\n') {
		if(canvas.fnbuf[0] == '\0') {
			strcpy(canvas.fnbuf, config.default_save_path);
		}

		if(canvas.save_mode == 1) {
			save_da(canvas.fnbuf);
		} else {
			load_da(canvas.fnbuf);
		}

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

void
norm_mouse(Mouse m)
{
    int i;
	
    if(m.buttons & 1){
        i = addbox(m.xy);
        if(i >= 0){
            canvas.selected = i;
            canvas.needredraw = 1;
        }
    }
    
    if(m.buttons & 2){ 
        i = boxat(m.xy);
        if(i >= 0){
            canvas.selected = i;
            Box *box = &canvas.boxes[i];

            int width = (box->type == T_BIGBOX) ? config.bigbox_width : config.box_width;
            int height = (box->type == T_BIGBOX) ? config.bigbox_height : config.box_height;
            
            while(m.buttons & 2){
                box->pos = subpt(m.xy, Pt(width/2, height/2));
                if(canvas.gridsnap){
                    box->pos.x = (box->pos.x / canvas.gridsize) * canvas.gridsize;
                    box->pos.y = (box->pos.y / canvas.gridsize) * canvas.gridsize;
                }
                box->r = Rect(box->pos.x, box->pos.y,
                    box->pos.x + width, box->pos.y + height);
                redraw();
                m = emouse();
            }
            canvas.needredraw = 1;
        }
    }
    
    if(m.buttons & 4){ 
        i = boxat(m.xy);
        if(i >= 0){
            delbox(i);
            if(canvas.selected == i)
                canvas.selected = -1;
            canvas.needredraw = 1;
        }
    }
}

void
fn_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);

	canvas.needredraw = 1;
}

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

void
ctl_save(char **args, int nargs)
{
	if(nargs >= 1) {
		save_da(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;

		CtlHandler *h;
		for(h = ctl_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, "dynamica") < 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(&canvas, 0, sizeof(canvas));
	canvas.selected = -1;
	canvas.fnkey = 0;
	canvas.current_mode = 0;
	canvas.current_color = 8;
	canvas.next_box_type = T_BOX;
	canvas.gridsize = config.gridsize;
	canvas.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) {
			canvas.needredraw = 1;
		}

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