shithub: da

Download patch

ref: 6891d9b8fd7a7e7cccb11095b467aac9a0a2feec
author: glenda <glenda@krsna>
date: Tue Aug 19 18:20:03 EDT 2025

new-dynamic-app

--- /dev/null
+++ b/da.c
@@ -1,0 +1,1297 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <event.h>
+#include <keyboard.h>
+#include <bio.h>
+#include <ctype.h>
+
+#define CONFIG_FILE 						"/usr/glenda/da/da.conf"
+#define DEFAULT_CTL_FILE 					"/usr/glenda/da/da.ctl"
+#define DEFAULT_SAVE_PATH 					"/usr/glenda/da/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:/usr/glenda/da/  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, "/usr/glenda/da/")) {
+			strcat(canvas.fnbuf, "/usr/glenda/da/");
+			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;
+		}
+	}
+}
\ No newline at end of file
--- /dev/null
+++ b/mkfile
@@ -1,0 +1,26 @@
+
+</$objtype/mkfile
+
+# Compiler flags
+CFLAGS=-FVTw
+
+# Targets
+BIN=$home/bin/amd64
+TARG=da
+
+# Source files
+APP=$TARG.6
+
+# Build rules
+all:V: $BIN/$TARG
+
+$APP: $TARG.c
+	6c $CFLAGS $TARG.c
+
+$BIN/$TARG: $APP
+	6l -o $target $APP
+
+nuke:
+	rm -f *.6 $BIN/$TARG
+clean:
+	rm -f *.6
--