easydoneitCTui

Easydoneit Terminal UI
git clone https://noulin.net/git/easydoneitCTui.git
Log | Files | Refs | LICENSE

edCore.c (135248B)


      1 #include "libsheepyObject.h"
      2 #include "shpPackages/ini/src/ini.h"
      3 #include "edCore.h"
      4 
      5 /** @package edi_core
      6  *  Core module
      7  */
      8 
      9 /** user_interface is initialized in start() and changes the behavior of some functions to allow a functional user interface design. The default is not used, start() is always called before using edi_core.
     10  * Values: 'cli', 'web'
     11  */
     12 static char *user_interface       = NULL;
     13 
     14 /** path to easydoneit.ini
     15  * used in edi desktop
     16  */
     17 static char *inipath = NULL;
     18 smallDictt *ini      = NULL;
     19 
     20 char *data_location               = NULL;
     21 static char *data_location_tasks  = NULL;
     22 static char *data_location_groups = NULL;
     23 static char *data_location_tree   = NULL;
     24 
     25 char *saved_data_location         = NULL;
     26 
     27 /** Available databases */
     28 static smallArrayt *databases     = NULL;
     29 
     30 /** list selected databases */
     31 static smallDictt *selected_d     = NULL;
     32 smallArrayt *selected             = NULL;
     33 static smallArrayt *selected_path = NULL;
     34 /** default database where tasks are created */
     35 static char *default_add_in       = NULL;
     36 
     37 /** add new tasks in 'bottom' of group or 'top' of group */
     38 char *add_top_or_bottom           = NULL;
     39 
     40 /** Autlink groups (array of tids) */
     41 static smallArrayt *autolink      = NULL;
     42 
     43 /** list of groups for edi ls -L, -La and -Lx (array of tids) */
     44 static smallArrayt *list_of_groups = NULL;
     45 
     46 /** characters for task ids */
     47 #define ID_BASE_DF ",0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"
     48 static char *ID_BASE              = ID_BASE_DF;
     49 static char *ID_BASE_STRING       = ID_BASE_DF;
     50 /** Length of order id in groups */
     51 #define ORDER_ID_LENGTH 8
     52 size_t BASE                       = sizeof(ID_BASE_DF)-1;
     53 
     54 char *TASK_STATUS[]               = {"  Active",
     55                                      "    Done",
     56                                      " Ongoing",
     57                                      " Pending",
     58                                      "Inactive",
     59                                      "    Void",
     60                                      NULL};
     61 char **TASK_STATUS_TRIM           = NULL;
     62 
     63 /** Sort order for sort_task_attributes function, ongoing, active, pending, done, inactive, void */
     64 static u8 SORT_TASK_ORDER[]       = {2,0,3,1,4,5};
     65 
     66 /* TODO remove unused - static char *LIST_OPTIONS[]       = {"tids", */
     67 /*                                      "positions", */
     68 /*                                      "html"}; */
     69 static char *list_option          =  "tids";
     70 
     71 char *STATUS_FILTER_STATES[]      = {"enable","disable"};
     72 
     73 /** enables all status filters */
     74 smallArrayt *status_filters = NULL;
     75 
     76 /** status filter dictionary, initialized after loading ini file */
     77 smallDictt *status_filters_d= NULL;
     78 
     79 /** colors (default) */
     80 //static i16 no_color[4]            = {-1,-1,-1,255};
     81 //static i16 status_fgColors[6][4]  = {{0,0,0,255},{0,255,0,255},{255,128,0,255},{255,0,0,255},{192,192,192,255},{0,0,0,255}};
     82 static smallArrayt *no_color         = NULL;
     83 static smallArrayt *status_fgColors  = NULL;
     84 static smallDictt *status_fgColors_d = NULL;
     85 /** no background color by default */
     86 //static i16 status_bgColors[6][4]  = {{-1,-1,-1,255}};
     87 static smallArrayt *status_bgColors  = NULL;
     88 static smallDictt *status_bgColors_d = NULL;
     89 
     90 /** text editor in terminal - easydoneit.ini [settings] EDITOR */
     91 static char *editor               = "vi";
     92 
     93 /** Agenda generated by edi.in_cli */
     94 static smallArrayt *agenda        = NULL;
     95 
     96 /** user name */
     97 static char *user                 = NULL;
     98 
     99 /** user email */
    100 static char *email                = NULL;
    101 
    102 /** bookmarks */
    103 smallArrayt *bookmarks            = NULL;
    104 
    105 /** stats for statistics function */
    106 static smallDictt *stats          = NULL;
    107 
    108 /** total number of tasks for statistics function */
    109 static u64 stats_total            = 0;
    110 
    111 /** creation dates of tasks in statistics, used to find out oldest task */
    112 static smallArrayt *stats_creation_dates = NULL;
    113 
    114 /** timely state changes and creation dates
    115  * Type: dict of dict of integers
    116  * keys are dates
    117  * each dates is a dictionary with STATS_OVERTIME_KEYS keys
    118  * each key is the amount for states and creation
    119  */
    120 static smallDictt *stats_overtime = NULL;
    121 
    122 /** timely state changes and creation dates */
    123 //STATS_OVERTIME_KEYS  = [TASK_STATUS[i] for i in range(len(TASK_STATUS))] + ['Creation']
    124 
    125 /* Group directory, cache result to speed up color functions */
    126 static smallArrayt *group_directory_file_list = NULL;
    127 
    128 
    129 
    130 static char *html_header = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">\n\
    131 <html>\n\
    132   <head>\n\
    133     <meta http-equiv=\"Content-Type\" content=\"text/html;charset=utf-8\">\n\
    134     <title>Tasks</title>\n\
    135     <style type=\"text/css\">\n\
    136       .subject {text-align: left}\n\
    137       .plannedStartDateTime {text-align: right}\n\
    138       .priority {text-align: right}\n\
    139       .inactive {color: #5E5E5E}\n\
    140       .late {color: #A020F0}\n\
    141       .active {color: #000000}\n\
    142       .duesoon {color: #FF8000}\n\
    143       .overdue {color: #FF0000}\n\
    144       .completed {color: #037700}\n\
    145 \n\
    146       body {\n\
    147           color: #333;\n\
    148           background-color: white;\n\
    149           font: 11px verdana, arial, helvetica, sans-serif;\n\
    150       }\n\
    151 \n\
    152       /* Styles for the title and table caption */\n\
    153       h1, caption {\n\
    154           text-align: center;\n\
    155           font-size: 18px;\n\
    156           font-weight: 900;\n\
    157           color: #778;\n\
    158       }\n\
    159 \n\
    160       /* Styles for the whole table */\n\
    161       #table {\n\
    162           border-collapse: collapse;\n\
    163           border: 2px solid #ebedff;\n\
    164           margin: 10px;\n\
    165           padding: 0;\n\
    166       }\n\
    167 \n\
    168       /* Styles for the header row */\n\
    169       .header {\n\
    170           font: bold 12px/14px verdana, arial, helvetica, sans-serif;\n\
    171           color: #07a;\n\
    172           background-color: #ebedff;\n\
    173       }\n\
    174 \n\
    175       /* Mark the column that is sorted on */\n\
    176       #sorted {\n\
    177           text-decoration: underline;\n\
    178       }\n\
    179 \n\
    180       /* Styles for a specific column */\n\
    181       .subject {\n\
    182           font-weight: bold;\n\
    183       }\n\
    184 \n\
    185       /* Styles for regular table cells */\n\
    186       td {\n\
    187           padding: 5px;\n\
    188           border: 2px solid #ebedff;\n\
    189       }\n\
    190 \n\
    191       /* Styles for table header cells */\n\
    192       th {\n\
    193           padding: 5px;\n\
    194           border: 2px solid #ebedff;\n\
    195       }\n\
    196 \n\
    197     </style>\n\
    198   </head>\n\
    199   <body>\n\
    200   <h1>Tasks</h1>\n\
    201     <table border=\"1\" id=\"table\">\n\
    202       <thead>\n\
    203         <tr class=\"header\">\n\
    204           <th class=\"subject\" scope=\"col\">Subject</th>\n\
    205           <th class=\"description\" scope=\"col\">Description</th>\n\
    206         </tr>\n\
    207       </thead>\n\
    208       <tbody>";
    209 
    210 static char *html_footer = "      </tbody>\n\
    211     </table>\n\
    212   </body>\n\
    213 </html>";
    214 
    215 
    216 
    217 
    218 
    219 ////////////
    220 // MACROS
    221 ////////////
    222 
    223 /**
    224  * generate task path in var
    225  * tid is the task identity, should be declared in the context
    226  */
    227 #define genTaskPath(var, path)\
    228 	sprintf(var, "%s/" path, generate_task_path(tid));
    229 
    230 /**
    231  * declare var and generate path in easydoneit database
    232  * var is a local string
    233  * path is literal string representing a path in the task: "fgColor"
    234  */
    235 #define createTaskPath(var, path)\
    236 	char var[8192];\
    237 	genTaskPath(var, path)
    238 
    239 ////////////
    240 // MACROS END
    241 ////////////
    242 
    243 
    244 
    245 
    246 // git support
    247 void keepDir(char *path) {
    248 	FILE *f;
    249 	f = fopen(path, "w");
    250 	free(path);
    251 	fclose(f);
    252 }
    253 
    254 int mkdirFree(char *path) {
    255 	int r;
    256 	r = mkdirParents(path);
    257 	free(path);
    258 	return r;
    259 }
    260 
    261 int copyFree(char *s, char *d) {
    262 	int r;
    263 	r = copy(s, d);
    264 	freeManyS(s,d);
    265 	return r;
    266 }
    267 
    268 /** initializes path to data.
    269  * creates directories<br>
    270  * creates root group and task<br>
    271  * @ingroup EDI_CORE
    272  */
    273 void init(void) {
    274 	// Set global variables
    275 	smallArrayt *ts = allocG(rtSmallArrayt);
    276 	fromArrayG(ts, TASK_STATUS, 0);
    277 
    278 	status_filters_d     = allocG(rtSmallDictt);
    279 	zipG(status_filters_d, ts, status_filters);
    280 	//logVarG(status_filters_d);
    281 	status_fgColors_d    = allocG(rtSmallDictt);
    282 	zipG(status_fgColors_d, ts, status_fgColors);
    283 	//logVarG(status_fgColors_d);
    284 	status_bgColors_d    = allocG(rtSmallDictt);
    285 	zipG(status_bgColors_d, ts, status_bgColors);
    286 	//logVarG(status_bgColors_d);
    287 	terminateG(ts);
    288 
    289 	data_location_tasks  = appendG(data_location, "/tasks");
    290 	data_location_groups = appendG(data_location, "/groups");
    291 	data_location_tree   = appendG(data_location, "/tree");
    292 
    293 	if (not fileExistsG(data_location_tasks)) {
    294 		mkdirParentsG(data_location_tasks);
    295 		mkdirParentsG(data_location_groups);
    296 		mkdirParentsG(data_location_tree);
    297 		// git support
    298 		keepDir(appendG(data_location_tree, "/.keepDir"));
    299 		mkdirFree(appendG(data_location_groups, "/root"));
    300 
    301 		// Create root description in tasks
    302 		char *task_path = appendG(data_location_tasks, "/root");
    303 		mkdirParentsG(task_path);
    304 		// copy root description from script directory
    305 		char *d = shDirname(getProgPath());
    306 		copyFree(appendG(d, "/root.txt"), appendG(task_path, "/description.txt"));
    307 		free(d);
    308 
    309 		// Create status
    310 		char **l = NULL;
    311 		pushG(&l, TASK_STATUS[TASK_STATUS_VOID]);
    312 		char *tmp =  appendG(task_path, "/status");
    313 		writeFileG(l, tmp);
    314 		freeG(l);
    315 		free(task_path);
    316 	}
    317 
    318 	// git support - when a new database is created, the group folder is empty and not saved in git
    319 	// create the group folder when the database is a cloned git with missing group folder
    320 	if (not fileExistsG(data_location_groups)) {
    321 		mkdirParentsG(data_location_groups);
    322 	}
    323 	if (not fileExistsG(data_location_tree)) {
    324 		mkdirParentsG(data_location_tree);
    325 	}
    326 }
    327 
    328 /** log actions in database - data_location/log.txt
    329  * @param[in] s string without user name and timestamp
    330  * @ingroup EDI_CORE
    331  */
    332 void edi_log(const char *s) {
    333 	char *fn = appendG(data_location, "/log.txt");
    334 	FILE *f = fopen(fn, "a");
    335 	if (!f) {
    336 		goto retn;
    337 	}
    338 
    339 	char *sT = trimG(s);
    340 	// get current time
    341 	char *t  = timeToS(time(0));
    342 	fprintf(f, "%s - %s <%s> - %s\n", t, user, email, sT);
    343 	freeManyS(sT, t);
    344 	fclose(f);
    345 retn:
    346 	free(fn);
    347 }
    348 
    349 /** string returned from select_database */
    350 static char result_select_database[8192];
    351 
    352 /** select location database
    353  * @param[in] location database name
    354  * @ingroup EDI_CORE
    355  */
    356 const char *select_database(char *location) {
    357 
    358 	if (hasG(selected_d, location)) {
    359 		freeManyS(data_location, data_location_tasks, data_location_groups, data_location_tree);
    360 		data_location        = getNDupG(selected_d, rtChar, location);
    361 
    362 		data_location_tasks  = appendG(data_location, "/tasks");
    363 		data_location_groups = appendG(data_location, "/groups");
    364 		data_location_tree   = appendG(data_location, "/tree");
    365 
    366 		if (not fileExists(data_location)) {
    367 			strcpy(result_select_database, data_location);
    368 			strcat(result_select_database, " is unreachable");
    369 		}
    370 		else {
    371 			result_select_database[0] = 0;
    372 		}
    373 	}
    374 	else {
    375 		strcpy(result_select_database, location);
    376 		strcat(result_select_database, " is not found in the configuration (.easydoneit.ini)");
    377 	}
    378 	return result_select_database;
    379 }
    380 
    381 /** Mix foreground colors from task, groups and default colors.
    382  * @return color array
    383  * @param[in] tid task id
    384  * @ingroup EDI_CORE
    385  * Collect all defined colors in link groups.
    386  * For each group, the group gets the first color defined in the tree<br>
    387  * compute average color
    388  */
    389 smallArrayt *mix_fgcolors(const char *tid) {
    390 	// mix colors
    391 	// collect all defined colors in groups to root
    392 	createAllocateSmallArray(colors);
    393 	createAllocateSmallArray(color);
    394 	// root has no parent group, dont search parent group color
    395 	if (not eqG(tid, "root")) {
    396 		// find all groups - link and parent group
    397 		char taskPath[8192];
    398 		createAllocateSmallArray(groups);
    399 		if (is_linked(tid)) {
    400 			genTaskPath(taskPath, "groups/");
    401 			groups = readDirG(rtSmallArrayt, taskPath);
    402 		}
    403 		else {
    404 			pushG(groups, (char*)find_group_containing_task(tid));
    405 		}
    406 
    407 		// walk parent groups until a color or root is found
    408 		while (lenG(groups)) {
    409 			enum {search, found_color};
    410 			int status_color = search;
    411 			sprintf(taskPath, "%s/fgColor", generate_task_path(getG(groups, rtChar, 0)));
    412 			if (fileExists(taskPath)) {
    413 				createAllocateSmallString(f);
    414 				readFileG(f, taskPath);
    415 				smallArrayt *c  = splitG(f, ",");
    416 				// convert c strings to int
    417 				enumerateSmallArray(c, CP, ci) {
    418 					castS(cp, CP);
    419 					setG(c, ci, parseIntG(cp));
    420 					finishG(cp);
    421 				}
    422 				terminateG(f);
    423 				// -1 means no_color, mix colors
    424 				if (getG(c, rtI64 ,0) != -1) {
    425 					pushNFreeG(colors, c);
    426 					status_color = found_color;
    427 				}
    428 				else {
    429 					// c is not a color
    430 					terminateG(c);
    431 				}
    432 			}
    433 			if (status_color == search) {
    434 				char *parent_group = (char *) find_group_containing_task(getG(groups, rtChar, 0));
    435 				if (eqG(parent_group, "error")) {
    436 					// break infinite loop when there is an error in the database
    437 					break;
    438 				}
    439 				if (not eqG(parent_group, "root")) {
    440 					pushG(groups, parent_group);
    441 				}
    442 			}
    443 			delG(groups, 0, 1);
    444 		}
    445 		terminateG(groups);
    446 
    447 		// compute average color
    448 		if (lenG(colors)) {
    449 			pushG(color, 0);pushG(color, 0);pushG(color, 0);pushG(color, 0);
    450 			forEachSmallArray(colors, C) {
    451 				cast(smallArrayt*, c, C);
    452 				range(i, lenG(c)) {
    453 					*getG(color, rtI32P, i) += getG(c, rtI32, i);
    454 				}
    455 				finishG(C);
    456 			}
    457 			size_t colorCount = lenG(colors);
    458 			range(i, lenG(color)) {
    459 				*getG(color, rtI32P, i) /= colorCount;
    460 			}
    461 		}
    462 	}
    463 	if (not lenG(colors)) {
    464 		// no defined colors, use default color for status
    465 		char *task_status = (char *)get_status(tid);
    466 		terminateG(color);
    467 		color                   = getNDupG(status_fgColors_d, rtSmallArrayt, task_status);
    468 	}
    469 	terminateG(colors);
    470 	return color;
    471 }
    472 
    473 /** get color for task tid
    474  * @return color array
    475  *# @param[in] tid task id
    476  * @ingroup EDI_CORE
    477  * when no color is set, mix colors from groups or use defaults
    478  */
    479 smallArrayt *get_forground_color(const char *tid) {
    480 	//color = no_color
    481 	smallArrayt *color = NULL;
    482 	createTaskPath(fgPath, "fgColor");
    483 
    484 	if (fileExists(fgPath)) {
    485 		createAllocateSmallArray(color);
    486 		createAllocateSmallString(f);
    487 		readFileG(f, fgPath);
    488 		smallArrayt *c  = splitG(f, ",");
    489 		// convert c strings to int
    490 		forEachSmallArray(c, CP) {
    491 			castS(cp, CP);
    492 			pushG(color, parseIntG(cp));
    493 			finishG(cp);
    494 		}
    495 		terminateManyG(f, c);
    496 		// -1 means no_color, mix colors
    497 		if (getG(color, rtI64 ,0) != -1) {
    498 			terminateG(color);
    499 			color = mix_fgcolors(tid);
    500 		}
    501 	}
    502 	else {
    503 		color   = mix_fgcolors(tid);
    504 	}
    505 	if (not color) {
    506 		color = dupG(no_color);
    507 	}
    508 	return color;
    509 }
    510 
    511 /** set color for task tid
    512  * @param[in] tid task id
    513  * @param[in] color_s color array
    514  * @ingroup EDI_CORE
    515  */
    516 void set_forground_color(const char *tid , const char *color_s) {
    517 	createTaskPath(fgPath, "fgColor");
    518 	writeFileG(color_s, fgPath);
    519 	sprintf(fgPath, "set foreground color in %s to %s", tid,color_s);
    520 	edi_log(fgPath);
    521 }
    522 
    523 /** remove foreground color for task tid
    524  * @param[in] tid task id
    525  * @ingroup EDI_CORE
    526  */
    527 void remove_foreground_color(const char *tid) {
    528 	createTaskPath(fgPath, "fgColor");
    529 	if (fileExists(fgPath)) {
    530 		rmAllG(fgPath);
    531 	}
    532 }
    533 
    534 /** Mix background colors from task, groups and default colors.
    535  * @return color array
    536  * @param[in] tid task id
    537  * @ingroup EDI_CORE
    538  * Collect all defined colors in link groups.
    539  * For each group, the group gets the first color defined in the tree<br>
    540  * compute average color
    541  */
    542 smallArrayt *mix_bgcolors(const char *tid) {
    543 	// mix colors
    544 	// collect all defined colors in groups to root
    545 	createAllocateSmallArray(colors);
    546 	createAllocateSmallArray(color);
    547 	// root has no parent group, dont search parent group color
    548 	if (not eqG(tid, "root")) {
    549 		// find all groups - link and parent group
    550 		char taskPath[8192];
    551 		createAllocateSmallArray(groups);
    552 		if (is_linked(tid)) {
    553 			genTaskPath(taskPath, "groups/");
    554 			groups = readDirG(rtSmallArrayt, taskPath);
    555 		}
    556 		else {
    557 			pushG(groups, (char*)find_group_containing_task(tid));
    558 		}
    559 
    560 		// walk parent groups until a color or root is found
    561 		while (lenG(groups)) {
    562 			enum {search, found_color};
    563 			int status_color = search;
    564 			sprintf(taskPath, "%s/bgColor", generate_task_path(getG(groups, rtChar, 0)));
    565 			if (fileExists(taskPath)) {
    566 				createAllocateSmallString(f);
    567 				readFileG(f, taskPath);
    568 				smallArrayt *c  = splitG(f, ",");
    569 				// convert c strings to int
    570 				enumerateSmallArray(c, CP, ci) {
    571 					castS(cp, CP);
    572 					setG(c, ci, parseIntG(cp));
    573 					finishG(cp);
    574 				}
    575 				terminateG(f);
    576 				// -1 means no_color, mix colors
    577 				if (getG(c, rtI64 ,0) != -1) {
    578 					pushNFreeG(colors, c);
    579 					status_color = found_color;
    580 				}
    581 				else {
    582 					// c is not a color
    583 					terminateG(c);
    584 				}
    585 			}
    586 			if (status_color == search) {
    587 				char *parent_group = (char *) find_group_containing_task(getG(groups, rtChar, 0));
    588 				if (eqG(parent_group, "error")) {
    589 					// break infinite loop when there is an error in the database
    590 					break;
    591 				}
    592 				if (not eqG(parent_group, "root")) {
    593 					pushG(groups, parent_group);
    594 				}
    595 			}
    596 			delG(groups, 0, 1);
    597 		}
    598 		terminateG(groups);
    599 
    600 		// compute average color
    601 		if (lenG(colors)) {
    602 			pushG(color, 0);pushG(color, 0);pushG(color, 0);pushG(color, 0);
    603 			forEachSmallArray(colors, C) {
    604 				cast(smallArrayt*, c, C);
    605 				range(i, lenG(c)) {
    606 					*getG(color, rtI32P, i) += getG(c, rtI32, i);
    607 				}
    608 				finishG(C);
    609 			}
    610 			size_t colorCount = lenG(colors);
    611 			range(i, lenG(color)) {
    612 				*getG(color, rtI32P, i) /= colorCount;
    613 			}
    614 		}
    615 	}
    616 	if (not lenG(colors)) {
    617 		// no defined colors, use default color for status
    618 		char *task_status = (char *)get_status(tid);
    619 		terminateG(color);
    620 		color                   = getNDupG(status_bgColors_d, rtSmallArrayt, task_status);
    621 	}
    622 	return color;
    623 }
    624 
    625 /** get color for task tid
    626  * @return color array
    627  * @param[in] tid task id
    628  * @ingroup EDI_CORE
    629  * when no color is set, mix colors from groups or use defaults
    630  */
    631 smallArrayt *get_background_color(const char *tid) {
    632 	//color = no_color
    633 	smallArrayt *color = NULL;
    634 	createTaskPath(bgPath, "bgColor");
    635 
    636 	if (fileExists(bgPath)) {
    637 		createAllocateSmallArray(color);
    638 		createAllocateSmallString(f);
    639 		readFileG(f, bgPath);
    640 		smallArrayt *c  = splitG(f, ",");
    641 		// convert c strings to int
    642 		forEachSmallArray(c, CP) {
    643 			castS(cp, CP);
    644 			pushG(color, parseIntG(cp));
    645 			finishG(cp);
    646 		}
    647 		terminateManyG(f, c);
    648 		// -1 means no_color, mix colors
    649 		if (getG(color, rtI64 ,0) != -1) {
    650 			terminateG(color);
    651 			color = mix_bgcolors(tid);
    652 		}
    653 	}
    654 	else {
    655 		color   = mix_bgcolors(tid);
    656 	}
    657 	if (not color) {
    658 		color = dupG(no_color);
    659 	}
    660 	return color;
    661 }
    662 
    663 /** set color for task tid
    664  * @return color array
    665  * @param[in] tid task id
    666  * @param[in] color_s color array
    667  * @ingroup EDI_CORE
    668  */
    669 void set_background_color(const char *tid, const char *color_s) {
    670 	createTaskPath(bgPath, "bgColor");
    671 	writeFileG(color_s, bgPath);
    672 	sprintf(bgPath, "set background color in %s to %s", tid,color_s);
    673 	edi_log(bgPath);
    674 }
    675 
    676 /** remove background color for task tid
    677  * @param[in] tid task id
    678  * @ingroup EDI_CORE
    679  */
    680 void remove_background_color(const char *tid) {
    681 	createTaskPath(bgPath, "bgColor");
    682 	if (fileExists(bgPath)) {
    683 		rmAllG(bgPath);
    684 	}
    685 }
    686 
    687 /** string returned from color_to_hex FF22AA */
    688 static char return_color_to_hex[7];
    689 
    690 /** converts color to hex format
    691  * @return color hex string
    692  * @param[in] color color array
    693  * @ingroup EDI_CORE
    694  */
    695 const char *color_to_hex(smallArrayt *color) {
    696 	createAllocateSmallArray(c);
    697 	forEachSmallArray(color, N) {
    698 		cast(smallIntt *, n, N);
    699 		if (getG(n, rtI64, unusedV) == -1) {
    700 			// -1 is no color, white
    701 			pushG(c, 255);
    702 		}
    703 		else {
    704 			pushNFreeG(c, dupG(n));
    705 		}
    706 		finishG(n);
    707 	}
    708 	sprintf(return_color_to_hex, "%02x%02x%02x", getG(c, rtI64, 0), getG(c, rtI64, 1), getG(c, rtI64, 2));
    709 	terminateG(c);
    710 	return return_color_to_hex;
    711 }
    712 
    713 
    714 /** string returned from generate_task_path */
    715 static char result_generate_task_path[8192];
    716 
    717 /** filesystem path for task in database tasks
    718  * @return path
    719  * @param[in] tid task id
    720  * @ingroup EDI_CORE
    721  */
    722 const char *generate_task_path(const char *tid) {
    723 	sprintf(result_generate_task_path, "%s/%s", data_location_tasks, tid);
    724 	return result_generate_task_path;
    725 }
    726 
    727 /** string returned from generate_group_path */
    728 static char result_generate_group_path[8192];
    729 
    730 /** filesystem path for group in database groups
    731  * @return path
    732  * @param[in] tid task id
    733  * @ingroup EDI_CORE
    734  */
    735 const char *generate_group_path(const char *tid) {
    736 	sprintf(result_generate_group_path, "%s/%s/", data_location_groups, tid);
    737 	return result_generate_group_path;
    738 }
    739 
    740 
    741 static char return_get_status[64];
    742 
    743 /** get status for tid
    744  * @return status string
    745  * @param[in] tid task id
    746  * @ingroup EDI_CORE
    747  */
    748 const char *get_status(const char *tid) {
    749 	// open status file
    750 	createTaskPath(statusPath, "status");
    751 	createAllocateSmallString(status);
    752 	if (!readFileG(status, statusPath)) {
    753 		goto except;
    754 	}
    755 	if (lenG(status) > 63) {
    756 		// the status is too long, this is wrong
    757 		goto except;
    758 	}
    759 	strcpy(return_get_status, ssGet(status));
    760 	terminateG(status);
    761 	return return_get_status;
    762 except:
    763 	printf("%s is an invalid task.", generate_task_path(tid));
    764 	return TASK_STATUS[TASK_STATUS_VOID];
    765 }
    766 
    767 /** returns s
    768  * @return s
    769  * @param[in] tid task id
    770  * @param[in] s task title string
    771  * @ingroup EDI_CORE
    772  * Replaces generate_task_string_with_tid in GUI
    773  */
    774 smallStringt *passThroughTitle(const char *tid, smallStringt *s) {
    775 	return dupG(s);
    776 }
    777 
    778 /** creates a string with task id, status and string parameter
    779  * @return string
    780  * @param[in] tid task id
    781  * @param[in] s task title string
    782  * @ingroup EDI_CORE
    783  * Displays a task depending on edi_core.list_option
    784  */
    785 smallStringt *generate_task_string_with_tid(const char *tid, smallStringt *s) {
    786 	const char *status = get_status(tid);
    787 	smallStringt *r = NULL;
    788 	if (eqG(list_option, "tids")) {
    789 		// %*s to dynamically adjust id length
    790 		r = formatO("%" stringifyExpr(ID_LENGTH) "s - %s -   %s", tid, status, ssGet(s));
    791 	}
    792 	if (eqG(list_option, "positions")) {
    793 		if (is_this_task_a_group(tid)) {
    794 			// print tid for groups in the list
    795 			r = formatO("%" stringifyExpr(ID_LENGTH) "s - %s -   %s", tid, status, ssGet(s));
    796 		}
    797 		else {
    798 			// hide tids for normal tasks
    799 			r = formatO("%" stringifyExpr(ID_LENGTH) "s - %s -   %s", "", status, ssGet(s));
    800 		}
    801 	}
    802 	if (eqG(list_option, "md")) {
    803 		// print title and description
    804 		createTaskPath(taskPath, "description.txt");
    805 		createAllocateSmallArray(description);
    806 		readFileG(description, taskPath);
    807 		// remove title line
    808 		delG(description, 0 ,1);
    809 
    810 		// add <br> for long titles
    811 		smallStringt *j = joinG(description, "\n");
    812 		r = formatO("\n---\n#%s<br>\n%s", ssGet(s), ssGet(j));
    813 		terminateManyG(description, j);
    814 	}
    815 /* 	if list_option = 'rst' */
    816 /* 		# print title and description */
    817 /* 		f             = open generate_task_path(tid)+os.sep+'description.txt' */
    818 /* 		# remove title line */
    819 /* 		description_l = f readlines[1:] */
    820 /* 		f close */
    821 /*  */
    822 /* 		# convert urls to link */
    823 /* 		NOBRACKET = r'[^\]\[]*' */
    824 /* 		BRK = ( r'\[(' */
    825 /* 			+ (NOBRACKET + r'(\[')*6 */
    826 /* 			+ (NOBRACKET+ r'\])*')*6 */
    827 /* 			+ NOBRACKET + r')\]' ) */
    828 /* 		NOIMG = r'(?<!\!)' */
    829 /* 		LINK_RE = NOIMG + BRK + \ */
    830 /* 		r'''\(\s*(<.*?>|((?:(?:\(.*?\))|[^\(\)]))*?)\s*((['"])(.*?)\12\s*)?\)''' */
    831 /* 		# [text](url) or [text](<url>) or [text](url "title") */
    832 /*  */
    833 /* 		compiled_re = re.compile("^(.*?)%s(.*?)$" % LINK_RE, re.DOTALL | re.UNICODE) */
    834 /*  */
    835 /* 		for line_index,l in enumerate(description_l) */
    836 /* 			if 'http' in l */
    837 /* 				try */
    838 /* 					match      = compiled_re.match(l) */
    839 /* 					url        = match.group(9) */
    840 /* 					text       = match.group(2) */
    841 /* 					before_url = match.group(1) */
    842 /* 					#title      = match.group(13) */
    843 /* 					after_url  = match.group(14) */
    844 /*  */
    845 /* 					# added a space after text because rst complains when there is no space before < */
    846 /* 					# delimiting the url */
    847 /* 					l = '%s`%s <%s>`_%s'% (before_url, text, url, after_url) */
    848 /* 				except */
    849 /* 					# not used, information */
    850 /* 					link_status = 'WARNING: the link is not in format [title](url)' */
    851 /* 			description_l[line_index] = l */
    852 /*  */
    853 /* 		description   = ''.join(description_l) */
    854 /*  */
    855 /* 		tid           = ' ' */
    856 /* 		# remove position and type (GROUP, LINK) from title */
    857 /* 		title         = s[11:] lstrip */
    858 /* 		r             = '%s\n'%title + '='*len(title) + '\n%s\n\n'%description */
    859 /* 	if list_option = 'html' */
    860 /* 		#<tr bgcolor="#FEFFCC"> */
    861 /* 		#  <td class="subject">          <font color="#000000">t1</font></td> */
    862 /* 		#  <td class="description">          <font color="#000000">&nbsp;</font></td> */
    863 /* 		#</tr> */
    864 /* #:define read_description */
    865 /* 		# convert < to &lt; and > &gt; to ignore html tags in title line */
    866 /* 		s             = s.replace('<','&lt;').replace('>','&gt;') */
    867 /* 		f             = open generate_task_path(tid)+os.sep+'description.txt' */
    868 /* 		# remove title line, convert < to &lt; and > &gt; to ignore html tags */
    869 /* 		description_l = ['%s<br>'%i rstrip.replace('<','&lt;').replace('>','&gt;') for i in f readlines[1:]] */
    870 /* 		f close */
    871 /*  */
    872 /* 		# convert urls to link */
    873 /* 		NOBRACKET = r'[^\]\[]*' */
    874 /* 		BRK = ( r'\[(' */
    875 /* 			+ (NOBRACKET + r'(\[')*6 */
    876 /* 			+ (NOBRACKET+ r'\])*')*6 */
    877 /* 			+ NOBRACKET + r')\]' ) */
    878 /* 		NOIMG = r'(?<!\!)' */
    879 /* 		LINK_RE = NOIMG + BRK + \ */
    880 /* 		r'''\(\s*(<.*?>|((?:(?:\(.*?\))|[^\(\)]))*?)\s*((['"])(.*?)\12\s*)?\)''' */
    881 /* 		# [text](url) or [text](<url>) or [text](url "title") */
    882 /*  */
    883 /* 		compiled_re = re.compile("^(.*?)%s(.*?)$" % LINK_RE, re.DOTALL | re.UNICODE) */
    884 /*  */
    885 /* 		for line_index,l in enumerate(description_l) */
    886 /* 			if 'http' in l */
    887 /* 				try */
    888 /* 					match      = compiled_re.match(l) */
    889 /* 					url        = match.group(9) */
    890 /* 					text       = match.group(2) */
    891 /* 					before_url = match.group(1) */
    892 /* 					#title      = match.group(13) */
    893 /* 					after_url  = match.group(14) */
    894 /*  */
    895 /* 					l = '%s<a href="%s">%s</a>%s'% (before_url, url, text, after_url) */
    896 /* 				except */
    897 /* 					# not used, information */
    898 /* 					link_status = 'WARNING: the link is not in format [title](url)' */
    899 /* 			description_l[line_index] = l */
    900 /*  */
    901 /* 		description   = '\n'.join(description_l) */
    902 /* #:end */
    903 /*  */
    904 /* 		if is_this_task_a_group(tid) and user_interface = 'web' */
    905 /* 			subject       = '<a href="edi_web.py?tid=%s">%s -   %s</a>'%(tid,status,s) */
    906 /* 		else */
    907 /* 			subject       = '%s -   %s'%(status,s) */
    908 /* 		fg            = color_to_hex(get_forground_color(tid)) */
    909 /* 		bg            = color_to_hex(get_background_color(tid)) */
    910 /* 		r='        <tr bgcolor="#%s">\n          <td class="subject">          <font color="#%s">%s</font></td>\n          <td class="description">          <font color="#%s">%s</font></td>\n        </tr>' % (bg, fg, subject, fg, description) */
    911 	return r;
    912 }
    913 
    914 /** creates a string with task id, status and string parameter
    915  * @return list of strings
    916  * @param[in] tid task id
    917  * @param[in] s task title string
    918  * @ingroup EDI_CORE
    919  * Displays a group depending on edi_core.list_option
    920  */
    921 smallStringt *generate_group_string_with_tid (const char *tid, smallStringt *s) {
    922 	const char *status = get_status(tid);
    923 	smallStringt *r = NULL;
    924 	if ((eqG(list_option, "tids")) or (eqG(list_option, "positions"))) {
    925 		// %*s to dynamically adjust id length
    926 		r = formatO("%" stringifyExpr(ID_LENGTH) "s - %s -   %s", tid, status, ssGet(s));
    927 	}
    928 /* 	if list_option = 'md' */
    929 /* 		tid         = ' ' */
    930 /* 		# print group title only */
    931 /* 		r           = '#%s'%s.replace('  0 - GROUP','') */
    932 /* 		if agenda */
    933 /* 			r += '\n---\n# Agenda\n%s' % '\n\n'.join(agenda) */
    934 /* 	if list_option = 'rst' */
    935 /* 		tid         = ' ' */
    936 /* 		# print group title only */
    937 /* 		title       = s.replace('  0 - GROUP','') */
    938 /* 		r           = '='*len(title) + '\n%s\n'%title + '='*len(title) + '\n\n' */
    939 /* 		if agenda */
    940 /* 			r += 'Agenda\n======\n%s\n\n' % '\n'.join(agenda) */
    941 /* 		else */
    942 /* 			r += '\n.. contents::\n\n' */
    943 /* 	if list_option = 'html' */
    944 /* 		#<tr bgcolor="#FEFFCC"> */
    945 /* 		#  <td class="subject">          <font color="#000000">t1</font></td> */
    946 /* 		#  <td class="description">          <font color="#000000">&nbsp;</font></td> */
    947 /* 		#</tr> */
    948 /* 		# display description in html */
    949 /* #:read_description */
    950 /*  */
    951 /* 		subject     = '%s - %s'%(status, s) */
    952 /* 		fg          = color_to_hex(get_forground_color(tid)) */
    953 /* 		bg          = color_to_hex(get_background_color(tid)) */
    954 /* 		r='        <tr bgcolor="#%s">\n          <td class="subject">          <font color="#%s">%s</font></td>\n          <td class="description">          <font color="#%s">%s</font></td>\n        </tr>' % (bg, fg, subject, fg, description) */
    955 	return r;
    956 }
    957 
    958 /** determines if task is a group
    959  * @return empty string or 'this task is a group'
    960  * @param[in] tid task id
    961  * @ingroup EDI_CORE
    962  */
    963 bool is_this_task_a_group(const char *tid) {
    964 	// Identify if task is a group
    965 	bool is_a_group     = false;
    966 	// list groups
    967 	smallArrayt *groups = readDirDirG(rtSmallArrayt, data_location_groups);
    968 	if (binarySearchG(groups, tid) != -1) {
    969 		is_a_group = true;
    970 	}
    971 	terminateG(groups);
    972 	return is_a_group;
    973 }
    974 
    975 /** get task title
    976  * @return first line of task description
    977  * @param[in] tid task id
    978  * @ingroup EDI_CORE
    979  */
    980 smallStringt *get_task_title(const char *tid) {
    981 	createTaskPath(task_path, "description.txt");
    982 	createAllocateSmallArray(f);
    983 	readFileG(f, task_path);
    984 	smallStringt *r = getNDupG(f, rtSmallStringt, 0);
    985 	terminateG(f);
    986 	if (not r) {
    987 		// empty description, return empty string
    988 		r = allocG("");
    989 	}
    990 	return r;
    991 }
    992 
    993 /** get creation date
    994  * @return array of date string and unix time
    995  * @param[in] tid task id
    996  * @ingroup EDI_CORE
    997  */
    998 smallArrayt *get_creation_date(const char *tid) {
    999 	createAllocateSmallArray(r);
   1000 	// Figure out creation time and modification times for description and status
   1001 	createTaskPath(task_path, "ctime.txt");
   1002 	if (not fileExists(task_path)) {
   1003 		pushG(r, "Not available");
   1004 		pushG(r, 0);
   1005 	}
   1006 	else {
   1007 		createAllocateSmallString(task_ctime);
   1008 		readFileG(task_ctime, task_path);
   1009 		pushG(r, task_ctime);
   1010 		struct tm tm;
   1011 		if (not strptime(ssGet(task_ctime), "%Y-%m-%d %H:%M", &tm)) {
   1012 			// string time failed to parse
   1013 			pushG(r, 0);
   1014 		}
   1015 		else {
   1016 			time_t t = mktime(&tm);
   1017 			pushG(r, (i64) t);
   1018 		}
   1019 		finishG(task_ctime);
   1020 	}
   1021 	return r;
   1022 }
   1023 
   1024 /** get media
   1025  * @return media file name
   1026  * @param[in] tid task id
   1027  * @ingroup EDI_CORE
   1028  */
   1029 smallArrayt *get_media(const char *tid) {
   1030 	createAllocateSmallArray(r);
   1031 	createTaskPath(task_path, "media");
   1032 	if (fileExists(task_path)) {
   1033 		smallArrayt *files = readDirG(rtSmallArrayt, task_path);
   1034 		smallStringt *fn = getG(files, rtSmallStringt, 0);
   1035 		smallArrayt *spl = splitG(fn, ".");
   1036 		pushG(r, getG(spl, rtChar, 0));
   1037 		pushNFreeG(r, catS(task_path, "/", getG(files, rtChar, 0)));
   1038 		terminateManyG(files, fn, spl);
   1039 	}
   1040 	else {
   1041 		pushG(r, "None");
   1042 	}
   1043 	return r;
   1044 }
   1045 
   1046 /** remove media
   1047  * @param[in] tid task id
   1048  * @ingroup EDI_CORE
   1049  */
   1050 void remove_media(const char *tid) {
   1051 	createTaskPath(task_path, "media");
   1052 	if (fileExists(task_path)) {
   1053 		rmAll(task_path);
   1054 	}
   1055 }
   1056 
   1057 
   1058 /** set media, one media file per task
   1059  * @return media array [media type (sound, image), file name]
   1060  * @param[in] tid task id
   1061  * @param[in] filename media file to copy to task
   1062  * @ingroup EDI_CORE
   1063  */
   1064 smallArrayt *set_media(const char *tid, const char *filename) {
   1065 	createAllocateSmallArray(r);
   1066 
   1067 	createTaskPath(task_path, "media");
   1068 	char *fn  = trimG(filename);
   1069 	if (endsWithG(fn, "wav")) {
   1070 		//#:define create_media_folder
   1071 		if (not fileExists(task_path)) {
   1072 			// create media folder
   1073 			mkdirParentsG(task_path);
   1074 		}
   1075 		//#:end
   1076 		else {
   1077 			// media exists, check type
   1078 			smallArrayt *files = readDirG(rtSmallArrayt, task_path);
   1079 			if (not endsWithG(getG(files, rtChar, 0), "wav")) {
   1080 				smallStringt *s = formatO("%s is not a sound.", tid);
   1081 				pushNFreeG(r, s);
   1082 				terminateG(files);
   1083 				goto end;
   1084 			}
   1085 			terminateG(files);
   1086 		}
   1087 		// sound file
   1088 		char fp[8192];
   1089 		sprintf(fp, "%s/sound.wav", task_path);
   1090 		copy(filename, fp);
   1091 		pushG(r, "sound.wav");
   1092 	}
   1093 	if (endsWithG(fn, "jpg")) {
   1094 		//#:create_media_folder
   1095 		if (not fileExists(task_path)) {
   1096 			// create media folder
   1097 			mkdirParentsG(task_path);
   1098 		}
   1099 		else {
   1100 			// media exists, check type
   1101 			smallArrayt *files = readDirG(rtSmallArrayt, task_path);
   1102 			if (not endsWithG(getG(files, rtChar, 0), "jpg")) {
   1103 				smallStringt *s = formatO("%s is not an image.", tid);
   1104 				pushNFreeG(r, s);
   1105 				terminateG(files);
   1106 				goto end;
   1107 			}
   1108 			terminateG(files);
   1109 		}
   1110 		// image file
   1111 		char fp[8192];
   1112 		sprintf(fp, "%s/image.jpg", task_path);
   1113 		copy(filename, fp);
   1114 		pushG(r, "image.jpg");
   1115 	}
   1116 end:
   1117 	free(fn);
   1118 	if (not lenG(r)) {
   1119 		smallStringt *s = formatO("%s is not a supported media file.", filename);
   1120 		pushNFreeG(r, s);
   1121 	}
   1122 	else {
   1123 		char log[8192];
   1124 		sprintf(log, "added media %s in task %s", filename, tid);
   1125 		edi_log(log);
   1126 	}
   1127 	return r;
   1128 }
   1129 
   1130 /** get attachments
   1131  * @return attachment file names
   1132  * @param[in] tid task id
   1133  * @ingroup EDI_CORE
   1134  */
   1135 smallArrayt *get_attachments(const char *tid) {
   1136 	smallArrayt *r;
   1137 	createTaskPath(task_path, "attachments/");
   1138 	if (fileExists(task_path)) {
   1139 		// list files in attachments folder and prepend task_path
   1140 		// r holds the paths to the attachments
   1141 		r = readDirAllG(rtSmallArrayt, task_path);
   1142 		enumerateSmallArray(r, F, i) {
   1143 			castS(f, F);
   1144 			prependG(f, task_path);
   1145 			setNFreePG(r, i, f);
   1146 		}
   1147 	}
   1148 	else {
   1149 		initiateG(&r);
   1150 		pushG(r, "None");
   1151 	}
   1152 	return r;
   1153 }
   1154 
   1155 /** remove attachments
   1156  * @param[in] tid task id
   1157  * @ingroup EDI_CORE
   1158  */
   1159 void remove_attachments(const char *tid) {
   1160 	createTaskPath(task_path, "attachments");
   1161 	if (fileExists(task_path)) {
   1162 		rmAll(task_path);
   1163 	}
   1164 }
   1165 
   1166 
   1167 /** set attachments
   1168  * @return attachment file names
   1169  * @param[in] tid task id
   1170  * @param[in] array of filenames
   1171  *  @ingroup EDI_CORE
   1172  *
   1173  * With *, set_attachments copies multiple files at once
   1174  */
   1175 smallArrayt *set_attachments(const char *tid, smallArrayt *filenames) {
   1176 	createTaskPath(task_path, "attachments/");
   1177 	if (not fileExists(task_path)) {
   1178 		// create attachment folder
   1179 		mkdirParentsG(task_path);
   1180 	}
   1181 
   1182 	createAllocateSmallArray(r);
   1183 	forEachSmallArray(filenames, FN) {
   1184 		castS(fn, FN);
   1185 		if (not copyG(fn,task_path)) goto except;
   1186 		char log[8192];
   1187 		sprintf(log, "added attachment %s in task %s", ssGet(fn), tid);
   1188 		edi_log(log);
   1189 		// Remove path from fn, keep only filename
   1190 		char *s = basename(ssGet(fn));
   1191 		pushNFreeG(r, appendG(task_path, s));
   1192 		goto end;
   1193 		except:
   1194 			pushNFreeG(r, appendG("Failed to copy ", ssGet(fn)));
   1195 		end:
   1196 		finishG(fn);
   1197 	}
   1198 	return r;
   1199 }
   1200 
   1201 /** sort task attributes (ls in edi cli)
   1202  * @return sorted task list by state
   1203  * @param[in] result from list_group
   1204  * @ingroup EDI_CORE
   1205  */
   1206 smallArrayt *sort_task_attributes(smallArrayt *task_attributes) {
   1207 	createAllocateSmallArray(r);
   1208 	// Keep head group on top
   1209 	smallDictt *d = getG(task_attributes, rtSmallDictt, 0);
   1210 	if (eqG(getG(d, rtChar, "head"), "head group")) {
   1211 		pushNFreeG(r, dupG(d));
   1212 		delG(task_attributes, 0, 1);
   1213 	}
   1214 	finishG(d);
   1215 	range(s, COUNT_ELEMENTS(SORT_TASK_ORDER)) {
   1216 		forEachSmallArray(task_attributes, T) {
   1217 			cast(smallDictt *, t, T);
   1218 			if (eqG(getG(t, rtChar, "status"), TASK_STATUS[s])) {
   1219 				pushG(r, T);
   1220 			}
   1221 			finishG(T);
   1222 		}
   1223 	}
   1224 	return r;
   1225 }
   1226 
   1227 /** sort function for sorting an array of tasks by date from newest to oldest
   1228  * @return compare result
   1229  * @param[in] result from list_group
   1230  * @ingroup EDI_CORE
   1231  */
   1232 int sortdatefunc(smallDictt *x, smallDictt *y) {
   1233 	i64 xv = getG(x, rtI64, "ctime");
   1234 	i64 yv = getG(y, rtI64, "ctime");
   1235 
   1236 	if (xv == yv) return 0;
   1237 	if (yv > xv) return 1;
   1238 	else return -1;
   1239 }
   1240 
   1241 /** sort task attributes (ls in edi cli)
   1242  * @return sorted task list by date
   1243  * @param[in] result from list_group
   1244  * @ingroup EDI_CORE
   1245  */
   1246 smallArrayt *sort_task_attributes_by_date(smallArrayt *task_attributes) {
   1247 	createAllocateSmallArray(r);
   1248 	// Keep head group on top
   1249 	smallDictt *d = getG(task_attributes, rtSmallDictt, 0);
   1250 	if (eqG(getG(d, rtChar, "head"), "head group")) {
   1251 		pushNFreeG(r, dupG(d));
   1252 		delG(task_attributes, 0, 1);
   1253 	}
   1254 	finishG(d);
   1255 	// -1 to exclude the empty line
   1256 	smallArrayt *a = copyRngG(task_attributes, 0, -1);
   1257 	//TODO a.sort(sortdatefunc)
   1258 	//print task_attributes
   1259 	appendNSmashG(r, a);
   1260 	pushNFreeG(r, getG(task_attributes, rtBaset, -1));
   1261 	return r;
   1262 }
   1263 
   1264 /** Get task item in list_group format
   1265  * @return r dictionary representing a task like the items returned by the list_group function
   1266  * @param[in] tid task id
   1267  * @ingroup EDI_CORE
   1268  * for edi desktop
   1269  */
   1270 smallDictt *get_task_in_list_group_format(const char *tid) {
   1271 	createAllocateSmallDict(r);
   1272 	setG(r, "head", "element");
   1273 	setG(r, "tid", tid);
   1274 	char *group = "     ";
   1275 	if (is_this_task_a_group(tid)) {
   1276 		group = "GROUP";
   1277 	}
   1278 	if (is_linked(tid)) {
   1279 		group = " LINK";
   1280 	}
   1281 	setG(r, "group", group);
   1282 	// figure out position in group
   1283 	i64 current_position = -1;
   1284 	const char *group_path    = generate_group_path(find_group_containing_task(tid));
   1285 	smallArrayt *lGroups = readDirDirG(rtSmallArrayt, group_path);
   1286 	enumerateSmallArray(lGroups, FN, n) {
   1287 		castS(fn, FN);
   1288 		if (hasG(fn, tid)) {
   1289 			// found tid in list
   1290 			// position in list
   1291 			smallStringt *posS = copyRngG(fn, 0 , ORDER_ID_LENGTH);
   1292 			current_position = baseconvert_to_dec(ssGet(posS));
   1293 			terminateG(posS);
   1294 			break;
   1295 		}
   1296 		finishG(fn);
   1297 	}
   1298 	setG(r, "position", current_position);
   1299 	// store title line
   1300 	createTaskPath(task_path, "description.txt");
   1301 	FILE *f = fopen(task_path, "r");
   1302 	setNFreeG(r, "title", trimG(readLineG(rtSmallStringt, f)));
   1303 	fclose(f);
   1304 	setG(r, "status", get_status(tid));
   1305 	smallArrayt *cd = get_creation_date(tid);
   1306 	setG(r, "ctime", getG(cd, rtI64, 1));
   1307 	terminateManyG(lGroups, cd);
   1308 	return r;
   1309 }
   1310 
   1311 /** list id - status - group - first line from description
   1312  * @return list of tasks and groups title
   1313  * @param[in] tid task id
   1314  * @ingroup EDI_CORE
   1315  * items in groups have the format 'ORDER_ID''TASK_ID'<br>
   1316  * task 0 is group tittle<br>
   1317  *<br>
   1318  * result array elements:<br>
   1319  * {head :string, tid :string, position :value, group :string, title :string, status :string}<br>
   1320  * head tells if the element is a group title.<br>
   1321  *<br>
   1322  * example:<br>
   1323  *fTTB1KRWfDpoSR1_ -   Active -   GROUP Motivational Interviewing<br>
   1324  *62ZvFA_q0pCZFr0Y -   Active -         news<br>
   1325  */
   1326 smallArrayt *list_group(const char *tid) {
   1327 	createAllocateSmallArray(result);
   1328 	// list visible groups only
   1329 	const char *task_status = get_status(tid);
   1330 	if (getG(status_filters_d, rtI32, task_status) == ENABLE) {
   1331 		// List task titles in group
   1332 		const char *group_path  = generate_group_path(tid);
   1333 
   1334 		// List groups to indicate when a task is a group
   1335 		// Identify if task is a group
   1336 		smallArrayt *groups = readDirDirG(rtSmallArrayt, data_location_groups);
   1337 		// End Identify if task is a group
   1338 
   1339 		// print items in tid in order
   1340 		smallArrayt *tasksInGroup = readDirG(rtSmallArrayt, group_path);
   1341 		enumerateSmallArray(tasksInGroup, FN, n) {
   1342 			castS(fnp, FN);
   1343 			smallStringt *fn = allocG(basename(ssGet(fnp)));
   1344 			if (lenG(fn) != ORDER_ID_LENGTH+ID_LENGTH) {
   1345 				// verify filename format in groups, cifs creates temporary files, ignore them
   1346 				printf("EDI_ERROR: Database inconsistent, delete file - rm %s%s\n", group_path, ssGet(fn));
   1347 			}
   1348 			else {
   1349 				// print position in list
   1350 				smallStringt *posS = copyRngG(fn, 0 , ORDER_ID_LENGTH);
   1351 				i64 current_position = baseconvert_to_dec(ssGet(posS));
   1352 				terminateG(posS);
   1353 
   1354 				// Identify if task is a group
   1355 				// Remove order_id, keep task id only
   1356 				char *group = "     ";
   1357 				if (hasG(groups, ssGet(fn)+ORDER_ID_LENGTH)) {
   1358 					group = "GROUP";
   1359 				}
   1360 				if (is_linked(ssGet(fn)+ORDER_ID_LENGTH)) {
   1361 					group = " LINK";
   1362 				}
   1363 				// End Identify if task is a group
   1364 				// Get task title
   1365 				// Remove order_id, keep task id only
   1366 				char task_path[8192];
   1367 				sprintf(task_path, "%s/description.txt", generate_task_path(ssGet(fn)+ORDER_ID_LENGTH));
   1368 				FILE *f = fopen(task_path, "r");
   1369 				// Remove order_id, keep task id only
   1370 				const char *task_status = get_status(ssGet(fn)+ORDER_ID_LENGTH);
   1371 				if ((not n) and (not eqG(tid, "root"))) {
   1372 					// First task is group title
   1373 					// filter status, keep task if status filter is enabled
   1374 					if (getG(status_filters_d, rtI32, task_status) == ENABLE) {
   1375 						createAllocateSmallDict(task_d);
   1376 						setG(task_d, "head", "head group");
   1377 						setG(task_d, "tid", ssGet(fn)+ORDER_ID_LENGTH);
   1378 						setG(task_d, "position", current_position);
   1379 						setG(task_d, "group", group);
   1380 						setNFreeG(task_d, "title", trimG(readLineG(rtSmallStringt, f)));
   1381 						setG(task_d, "status", get_status(ssGet(fn)+ORDER_ID_LENGTH));
   1382 						smallArrayt *cd = get_creation_date(ssGet(fn)+ORDER_ID_LENGTH);
   1383 						setG(task_d, "ctime", getG(cd, rtI64, 1));
   1384 						terminateG(cd);
   1385 						pushNFreeG(result, task_d);
   1386 					}
   1387 				}
   1388 				else {
   1389 					// filter status, keep task if status filter is enabled
   1390 					if (getG(status_filters_d, rtI32, task_status) == ENABLE) {
   1391 						createAllocateSmallDict(task_d);
   1392 						setG(task_d, "head", "element");
   1393 						setG(task_d, "tid", ssGet(fn)+ORDER_ID_LENGTH);
   1394 						setG(task_d, "position", current_position);
   1395 						setG(task_d, "group", group);
   1396 						setNFreeG(task_d, "title", trimG(readLineG(rtSmallStringt, f)));
   1397 						setG(task_d, "status", get_status(ssGet(fn)+ORDER_ID_LENGTH));
   1398 						smallArrayt *cd = get_creation_date(ssGet(fn)+ORDER_ID_LENGTH);
   1399 						setG(task_d, "ctime", getG(cd, rtI64, 1));
   1400 						terminateG(cd);
   1401 						pushNFreeG(result, task_d);
   1402 					}
   1403 				}
   1404 				fclose(f);
   1405 			}
   1406 			terminateG(fn);
   1407 			finishG(fnp);
   1408 		}
   1409 		terminateManyG(groups, tasksInGroup);
   1410 		createAllocateSmallDict(task_d);
   1411 		setG(task_d, "head", "empty line");
   1412 		setG(task_d, "tid", "");
   1413 		setG(task_d, "position", 0);
   1414 		setG(task_d, "group", "");
   1415 		setG(task_d, "title", "");
   1416 		setG(task_d, "status", "");
   1417 		setG(task_d, "ctime", 0);
   1418 		pushNFreeG(result, task_d);
   1419 	}
   1420 	return result;
   1421 }
   1422 
   1423 smallArrayt *listBookmarks(void) {
   1424 	createAllocateSmallArray(result);
   1425 
   1426 	// check if there are bookmarks
   1427 	if (not eqG(getG(bookmarks, rtChar, 0), "")) {
   1428 
   1429 		enumerateSmallArray(bookmarks, FN, n) {
   1430 			castS(fn, FN);
   1431 
   1432 			// print position in list
   1433 			i64 current_position = n;
   1434 
   1435 			// setup database
   1436 			save_edi_core_data_location();
   1437 			setup_data_location_for_tid(ssGet(fn));
   1438 
   1439 			// Identify if task is a group
   1440 			// Remove order_id, keep task id only
   1441 			char *group = "     ";
   1442 			if (is_this_task_a_group(ssGet(fn))) {
   1443 				group = "GROUP";
   1444 			}
   1445 			if (is_linked(ssGet(fn))) {
   1446 				group = " LINK";
   1447 			}
   1448 			// End Identify if task is a group
   1449 			// Get task title
   1450 			char task_path[8192];
   1451 			sprintf(task_path, "%s/description.txt", generate_task_path(ssGet(fn)));
   1452 			FILE *f = fopen(task_path, "r");
   1453 			const char *task_status = get_status(ssGet(fn));
   1454 			// filter status, keep task if status filter is enabled
   1455 			if (getG(status_filters_d, rtI32, task_status) == ENABLE) {
   1456 				createAllocateSmallDict(task_d);
   1457 				setG(task_d, "head", "element");
   1458 				setG(task_d, "tid", ssGet(fn));
   1459 				setG(task_d, "position", current_position);
   1460 				setG(task_d, "group", group);
   1461 				setNFreeG(task_d, "title", trimG(readLineG(rtSmallStringt, f)));
   1462 				setG(task_d, "status", get_status(ssGet(fn)));
   1463 				smallArrayt *cd = get_creation_date(ssGet(fn));
   1464 				setG(task_d, "ctime", getG(cd, rtI64, 1));
   1465 				terminateG(cd);
   1466 				pushNFreeG(result, task_d);
   1467 			}
   1468 			fclose(f);
   1469 			restore_edi_core_data_location();
   1470 			finishG(fn);
   1471 		}
   1472 	}
   1473 	createAllocateSmallDict(task_d);
   1474 	setG(task_d, "head", "empty line");
   1475 	setG(task_d, "tid", "");
   1476 	setG(task_d, "position", 0);
   1477 	setG(task_d, "group", "");
   1478 	setG(task_d, "title", "");
   1479 	setG(task_d, "status", "");
   1480 	setG(task_d, "ctime", 0);
   1481 	pushNFreeG(result, task_d);
   1482 	return result;
   1483 }
   1484 
   1485 /** lists all items in the tid group
   1486  * @return list of tasks and groups title
   1487  * @param[in] tid task id
   1488  * @ingroup EDI_CORE
   1489  */
   1490 smallArrayt *list_tree(const char *tid) {
   1491 	createAllocateSmallArray(result);
   1492 	// walk_group is the list of groups to visit. FIFO
   1493 	createAllocateSmallArray(walk_group);
   1494 	pushG(walk_group, tid);
   1495 	// the while loop goes through all the group that are found
   1496 	while (lenG(walk_group)) {
   1497 		// list visible groups only
   1498 		const char *task_status = get_status(getG(walk_group, rtChar, 0));
   1499 		if (getG(status_filters_d, rtI32, task_status) == ENABLE) {
   1500 			// list items in first group
   1501 			smallArrayt *tasks = readDirG(rtSmallArrayt, generate_group_path(getG(walk_group, rtChar, 0)));
   1502 			appendNSmashG(result,  list_group(getG(walk_group, rtChar, 0)));
   1503 
   1504 			// add group found in first group
   1505 			forEachSmallArray(tasks, T) {
   1506 				castS(t, T);
   1507 				// Check tasks that are not title task in a group
   1508 				if (not eqG(ssGet(t)+ORDER_ID_LENGTH, getG(walk_group, rtChar, 0)) and is_this_task_a_group(ssGet(t)+ORDER_ID_LENGTH)) {
   1509 					pushG(walk_group, ssGet(t)+ORDER_ID_LENGTH);
   1510 				}
   1511 				finishG(t);
   1512 			}
   1513 			terminateG(tasks);
   1514 		}
   1515 
   1516 		// remove first group to list items in next group
   1517 		delG(walk_group, 0, 1);
   1518 	}
   1519 	terminateG(walk_group);
   1520 	return result;
   1521 }
   1522 
   1523 /** string returned from generate_id */
   1524 static char return_generate_id[ID_LENGTH+1];
   1525 
   1526 /** generates a random task id
   1527  * @return new task id
   1528  * @ingroup EDI_CORE
   1529  */
   1530 const char *generate_id(void) {
   1531 	return_generate_id[ID_LENGTH] = 0;
   1532 
   1533 	randomUrandomOpen();
   1534 	range(i, ID_LENGTH) {
   1535 		return_generate_id[i] = ID_BASE[randomChoice(BASE)];
   1536 	}
   1537 	return return_generate_id;
   1538 }
   1539 
   1540 /** string returned from baseconvert */
   1541 static char return_baseconvert[ORDER_ID_LENGTH+1];
   1542 
   1543 /** converts decimal number to BASE (base 65)
   1544  * @return string representing a number in base BASE
   1545  * @param[in] n integer
   1546  * @ingroup EDI_CORE
   1547  */
   1548 const char *baseconvert(i64 n) {
   1549 	if (n < 0) {
   1550 		return_baseconvert[0] = 0;
   1551 	}
   1552 	else {
   1553 		return_baseconvert[ORDER_ID_LENGTH] = 0;
   1554 		u8 i = ORDER_ID_LENGTH-1;
   1555 		while (1) {
   1556 			i64 r = n % BASE;
   1557 			return_baseconvert[i] = ID_BASE[r];
   1558 			i--;
   1559 			n = n / BASE;
   1560 			if (n == 0)
   1561 				break;
   1562 		}
   1563 		rangeDown(j, i+1) {
   1564 			return_baseconvert[j] = ID_BASE[0];
   1565 		}
   1566 	}
   1567 	return return_baseconvert;
   1568 }
   1569 
   1570 /** converts BASE number to decimal
   1571  * @return n integer
   1572  * @param[in] n string representing a number in base BASE
   1573  * @ingroup EDI_CORE
   1574  */
   1575 i64 baseconvert_to_dec(const char *n) {
   1576 	i64 r     = 0;
   1577 	i64 power = 1;
   1578 	rangeDown(digit, lenG(n)) {
   1579 		int i = 0;
   1580 		range(a, BASE) {
   1581 			if (ID_BASE_STRING[a] == getG(n, unusedV, digit)) {
   1582 				break;
   1583 			}
   1584 			i++;
   1585 		}
   1586 		r     += i * power;
   1587 		power *= BASE;
   1588 	}
   1589 	return r;
   1590 }
   1591 
   1592 /** string returned from add_task_to_group_folder */
   1593 static char return_add_task_to_group_folder[8192];
   1594 
   1595 /** add task to group folder in database groups
   1596  * @param[in] tid task id
   1597  * @param[in] group task id
   1598  * @ingroup EDI_CORE
   1599  * Tasks are added at the top or bottom of the list.
   1600  */
   1601 const char *add_task_to_group_folder(const char *tid, const char *group) {
   1602 	// Create an entry in group
   1603 	smallArrayt *tasks = NULL;
   1604 	const char *order_id;
   1605 	tasks = readDirG(rtSmallArrayt, generate_group_path(group));
   1606 	// Add +1 to last order_id to have the task last in the list
   1607 	if (tasks and (lenG(tasks) > 0)) {
   1608 		if ((lenG(tasks) == 1) or (eqG(add_top_or_bottom, "bottom"))) {
   1609 			// add tasks in bottom
   1610 			char *s              = copyRngG(getG(tasks, rtChar, -1), 0, ORDER_ID_LENGTH);
   1611 			order_id = baseconvert(baseconvert_to_dec(s)+1);
   1612 			free(s);
   1613 		}
   1614 		else {
   1615 			// add tasks on top
   1616 			// temporary orderid #
   1617 			char orderid_and_tid[ORDER_ID_LENGTH + ID_LENGTH + 1];
   1618 			range(i, ORDER_ID_LENGTH) {
   1619 				orderid_and_tid[i] = '#';
   1620 			}
   1621 			strcat(orderid_and_tid, tid);
   1622 			i64 to_pos;
   1623 			if (eqG(group, "root")) {
   1624 				to_pos = 0;
   1625 			}
   1626 			else {
   1627 				// add new tasks after group title
   1628 				to_pos = 1;
   1629 			}
   1630 
   1631 			injectSG(tasks, to_pos, orderid_and_tid);
   1632 
   1633 			// Move tasks
   1634 			const char *path = generate_group_path(group);
   1635 			enumerateSmallArray(tasks, T, n) {
   1636 				castS(t, T);
   1637 				if (n == to_pos) {
   1638 					// set orderid on top
   1639 					order_id = baseconvert(n);
   1640 				}
   1641 				else {
   1642 					char src[8192];
   1643 					sprintf(src, "%s/%s", path, ssGet(t));
   1644 					char dst[8192];
   1645 					sprintf(dst, "%s/%s%s", path, baseconvert(n), ssGet(t)+ORDER_ID_LENGTH);
   1646 					shRename(src, dst);
   1647 				}
   1648 				finishG(t);
   1649 			}
   1650 		}
   1651 	}
   1652 	else {
   1653 		// start at 0 when group is empty
   1654 		order_id = baseconvert(0);
   1655 	}
   1656 
   1657 	sprintf(return_add_task_to_group_folder, "%s/%s%s", generate_group_path(group), order_id, tid);
   1658 	// remove double slash that can come up
   1659 	char *tmp = return_add_task_to_group_folder;
   1660 	iUniqSlash(tmp);
   1661 	FILE *f = fopen(return_add_task_to_group_folder, "w");
   1662 	fclose(f);
   1663 
   1664 	// return return_add_task_to_group_folder to easily add new tasks to group_directory_file_list and save time
   1665 	return return_add_task_to_group_folder;
   1666 }
   1667 
   1668 /** creates a task and opens vi
   1669  * @param[in] group task id
   1670  * @ingroup EDI_CORE
   1671  */
   1672 const char *create_task(const char *group) {
   1673 	// Open text editor
   1674 	// create task in tasks folder
   1675 	//#:define create_task_part1
   1676 	const char *tid =  generate_id();
   1677 
   1678 	// Save text in tasks
   1679 	const char *task_path  = generate_task_path(tid);
   1680 	mkdirParentsG(task_path);
   1681 	//#:end
   1682 	// create an empty file (for windows and mac osx)
   1683 	createTaskPath(dPath, "description.txt");
   1684 	FILE *f = fopen(dPath, "w");
   1685 	fclose(f);
   1686 	if (hasG(editor, "echo TEST")) {
   1687 		// use os.system to be able to run edi cli unittests
   1688 		//char *c = catG(editor, dPath);
   1689 		char *c = catS("echo TEST > ", dPath);
   1690 		systemNFreeG(c);
   1691 	}
   1692 	else {
   1693 		// use subprocess.call to wait until the editor is closed (Windows and Mac OSX) because the task is deleted when the description is empty
   1694 		// create a list [command,param1,param2,task file] to allow options on command line to start the editor
   1695 		smallStringt *ss      = allocG(editor);
   1696 		smallArrayt *editor_l = splitG(ss, "/");
   1697 		terminateG(ss);
   1698 		// after last os.sep there is the command name and the option. Do this because there can be spaces in the path to the command
   1699 		ss                    = getG(editor_l, rtSmallStringt, -1);
   1700 		smallArrayt *options  = splitG(ss, " ");
   1701 		finishG(ss);
   1702 		setNFreeG(editor_l, -1, getNDupG(options, rtBaset, 0));
   1703 		delG(options, 0, 1);
   1704 		createAllocateSmallArray(command_line);
   1705 		ss                    = joinG(editor_l, "/");
   1706 		terminateG(editor_l);
   1707 		pushG(command_line, ss);
   1708 		appendNSmashG(command_line, options);
   1709 		rallocG(ss, dPath);
   1710 		pushNFreeG(command_line, ss);
   1711 		ss = joinG(command_line, " ");
   1712 		terminateG(command_line);
   1713 		systemNFreeG(ss);
   1714 	}
   1715 	if (not fileExists(dPath)) {
   1716 		// file doesnt exist, abort task creation
   1717 		rmAllG(task_path);
   1718 		return "no new task, the description is empty";
   1719 	}
   1720 
   1721 	// Save creation time
   1722 	//#:define save_task_creation_time
   1723 	time_t t = getModificationTimeG(dPath);
   1724 	char *s  = timeToS(t);
   1725 	genTaskPath(dPath, "ctime.txt");
   1726 	writeFileG(s, dPath);
   1727 	free(s);
   1728 	//#:end
   1729 
   1730 	// create status, active by default
   1731 	//#:define create_task_part2
   1732 	// Create status
   1733 	genTaskPath(dPath, "status");
   1734 	writeFileG(TASK_STATUS[TASK_STATUS_ACTIVE], dPath);
   1735 
   1736 	const char *tid_in_groups_path = add_task_to_group_folder(tid, group);
   1737 
   1738 	if (lenG(group_directory_file_list)) {
   1739 		pushG(group_directory_file_list, tid_in_groups_path);
   1740 	}
   1741 
   1742 	// autolink
   1743 	if (autolink) {
   1744 		forEachSmallArray(autolink, G) {
   1745 			castS(g, G);
   1746 			if (is_this_task_a_group(ssGet(g))) {
   1747 				// link to groups existing in current database
   1748 				add_task_reference_to_a_group(tid, g);
   1749 			}
   1750 			finishG(g);
   1751 		}
   1752 	}
   1753 	//#:end
   1754 	sprintf(dPath, "created %s in group %s", tid, group);
   1755 	edi_log(dPath);
   1756 	return tid;
   1757 }
   1758 
   1759 /** create task and copy text file
   1760  * @param[in] group task id
   1761  * @param[in] text_file filename of text file to copy
   1762  * @ingroup EDI_CORE
   1763  */
   1764 const char *add_task(const char *group, const char *text_file) {
   1765 	//#:create_task_part1
   1766 	const char *tid =  generate_id();
   1767 
   1768 	// Save text in tasks
   1769 	const char *task_path  = generate_task_path(tid);
   1770 	mkdirParentsG(task_path);
   1771 	createTaskPath(dPath, "description.txt");
   1772 	copy(text_file, dPath);
   1773 	//#:save_task_creation_time
   1774 	time_t t = getModificationTimeG(dPath);
   1775 	char *s  = timeToS(t);
   1776 	genTaskPath(dPath, "ctime.txt");
   1777 	writeFileG(s, dPath);
   1778 	free(s);
   1779 	//#:create_task_part2
   1780 	// Create status
   1781 	genTaskPath(dPath, "status");
   1782 	writeFileG(TASK_STATUS[TASK_STATUS_ACTIVE], dPath);
   1783 
   1784 	const char *tid_in_groups_path = add_task_to_group_folder(tid, group);
   1785 
   1786 	if (lenG(group_directory_file_list)) {
   1787 		pushG(group_directory_file_list, tid_in_groups_path);
   1788 	}
   1789 
   1790 	// autolink
   1791 	if (autolink) {
   1792 		forEachSmallArray(autolink, G) {
   1793 			castS(g, G);
   1794 			if (is_this_task_a_group(ssGet(g))) {
   1795 				// link to groups existing in current database
   1796 				add_task_reference_to_a_group(tid, g);
   1797 			}
   1798 			finishG(g);
   1799 		}
   1800 	}
   1801 	sprintf(dPath, "created %s in group %s", tid, group);
   1802 	edi_log(dPath);
   1803 	return tid;
   1804 }
   1805 
   1806 /** create task with description text
   1807  * @param[in] group task id
   1808  * @param[in] text string
   1809  * @ingroup EDI_CORE
   1810  */
   1811 const char *add_text(const char *group, smallStringt *text) {
   1812 	//#:create_task_part1
   1813 	const char *tid =  generate_id();
   1814 
   1815 	// Save text in tasks
   1816 	const char *task_path  = generate_task_path(tid);
   1817 	mkdirParentsG(task_path);
   1818 	createTaskPath(dPath, "description.txt");
   1819 	writeFileG(text, dPath);
   1820 	//#:save_task_creation_time
   1821 	time_t t = getModificationTimeG(dPath);
   1822 	char *s  = timeToS(t);
   1823 	genTaskPath(dPath, "ctime.txt");
   1824 	writeFileG(s, dPath);
   1825 	free(s);
   1826 	//#:create_task_part2
   1827 	// Create status
   1828 	genTaskPath(dPath, "status");
   1829 	writeFileG(TASK_STATUS[TASK_STATUS_ACTIVE], dPath);
   1830 
   1831 	const char *tid_in_groups_path = add_task_to_group_folder(tid, group);
   1832 
   1833 	if (lenG(group_directory_file_list)) {
   1834 		pushG(group_directory_file_list, tid_in_groups_path);
   1835 	}
   1836 
   1837 	// autolink
   1838 	if (autolink) {
   1839 		forEachSmallArray(autolink, G) {
   1840 			castS(g, G);
   1841 			if (is_this_task_a_group(ssGet(g))) {
   1842 				// link to groups existing in current database
   1843 				add_task_reference_to_a_group(tid, g);
   1844 			}
   1845 			finishG(g);
   1846 		}
   1847 	}
   1848 	sprintf(dPath, "created %s in group %s", tid, group);
   1849 	edi_log(dPath);
   1850 	return tid;
   1851 }
   1852 
   1853 /** create task and copy text file, filename is task title
   1854  * @param[in] group task id
   1855  * @param[in] text_file filename of text file to copy
   1856  * @ingroup EDI_CORE
   1857  */
   1858 const char *add_task_and_filename(const char *group, const char *text_file) {
   1859 	const char *tid = add_task(group,text_file);
   1860 
   1861 	// Add file name on first line
   1862 	createTaskPath(dPath, "description.txt");
   1863 	createAllocateSmallArray(f);
   1864 	readFileG(f, dPath);
   1865 	char *s = basename(text_file);
   1866 	prependG(f, s);
   1867 	writeFileG(f, dPath);
   1868 	terminateG(f);
   1869 	return tid;
   1870 }
   1871 
   1872 /** string returned from add_many_tasks_array */
   1873 static char return_add_many_tasks_array[100];
   1874 
   1875 /** create many tasks from a text array
   1876  * @param[in] group task id
   1877  * @param[in] text_array text array to copy
   1878  * @ingroup EDI_CORE
   1879  * Used directly in Easydoneit Desktop
   1880  */
   1881 const char *add_many_tasks_array(const char *group, smallArrayt *text_array) {
   1882 	return_add_many_tasks_array[0]  = 0;
   1883 
   1884 	// Copy a task to text string
   1885 	// Remove '#' from first line, first character position
   1886 	// Add task to database
   1887 	createAllocateSmallString(text);
   1888 	i64 number_of_tasks = 0;
   1889 	enum {START, WRITING_TASK};
   1890 	i8 status           = START;
   1891 	forEachSmallArray(text_array, L) {
   1892 		castS(l, L);
   1893 		if (eqG(l, "---")) {
   1894 			if (status != START) {
   1895 				// add task to database
   1896 				add_text(group,text);
   1897 				number_of_tasks++;
   1898 			}
   1899 			emptyG(text);
   1900 			status = START;
   1901 		}
   1902 		else {
   1903 			if ((status == START) and (ssGet(l)[0] == '#')) {
   1904 				delG(l, 0, 1);
   1905 			}
   1906 			appendNSmashG(text, dupG(l));
   1907 			status = WRITING_TASK;
   1908 		}
   1909 		finishG(l);
   1910 	}
   1911 
   1912 	if (status == WRITING_TASK) {
   1913 		// add task to database
   1914 		add_text(group,text);
   1915 		number_of_tasks++;
   1916 	}
   1917 	terminateG(text);
   1918 
   1919 	sprintf(return_add_many_tasks_array, "created %ld tasks in group %s", number_of_tasks, group);
   1920 	edi_log(return_add_many_tasks_array);
   1921 	return return_add_many_tasks_array;
   1922 }
   1923 
   1924 /** create many tasks from a text file
   1925  * @param[in] group task id
   1926  * @param[in] text_file filename of text file to copy
   1927  * @ingroup EDI_CORE
   1928  */
   1929 const char *add_many_tasks(const char *group, const char *text_file) {
   1930 
   1931 	// load file and call add_many_tasks_array
   1932 	createAllocateSmallArray(text_array);
   1933 	readFileG(text_array, text_file);
   1934 	const char *r = add_many_tasks_array(group, text_array);
   1935 	terminateG(text_array);
   1936 	return r;
   1937 }
   1938 
   1939 /** string returned from add_many_one_line_tasks_array */
   1940 static char return_add_many_one_line_tasks_array[100];
   1941 
   1942 /** create many one line tasks from a text array
   1943  * @param[in] group task id
   1944  * @param[in] text_array text array to copy
   1945  * @ingroup EDI_CORE
   1946  * Used directly in Easydoneit Desktop
   1947  */
   1948 const char *add_many_one_line_tasks_array(const char *group, smallArrayt *text_array) {
   1949 	return_add_many_one_line_tasks_array[0] = 0;
   1950 
   1951 	i64 number_of_tasks = 0;
   1952 	forEachSmallArray(text_array, L) {
   1953 		castS(l, L);
   1954 		trimG(l);
   1955 		add_text(group,l);
   1956 		number_of_tasks++;
   1957 		finishG(l);
   1958 	}
   1959 
   1960 	sprintf(return_add_many_one_line_tasks_array, "created %ld tasks in group %s", number_of_tasks,group);
   1961 	edi_log(return_add_many_one_line_tasks_array);
   1962 	return return_add_many_one_line_tasks_array;
   1963 }
   1964 
   1965 /** create many one line tasks from a text file
   1966  * @param[in] group task id
   1967  * @param[in] text_file filename of text file to copy
   1968  * @ingroup EDI_CORE
   1969  */
   1970 const char *add_many_one_line_tasks(const char *group, const char *text_file) {
   1971 
   1972 	createAllocateSmallArray(text_array);
   1973 	readFileG(text_array, text_file);
   1974 	const char *r = add_many_one_line_tasks_array(group, text_array);
   1975 	terminateG(text_array);
   1976 	return r;
   1977 }
   1978 
   1979 /** string returned from add_many_groups_from_text_array */
   1980 static char return_add_many_groups_from_text_array[100];
   1981 
   1982 /** create group from text array
   1983  * @param[in] group task id
   1984  * @param[in] text_array with group structure
   1985  * @ingroup EDI_CORE
   1986  * Used directly in Easydoneit Desktop
   1987  */
   1988 const char *add_many_groups_from_text_array(const char *group, smallArrayt *text_array) {
   1989 	return_add_many_groups_from_text_array[0] = 0;
   1990 
   1991 	i64 number_of_tasks = 0;
   1992 	createAllocateSmallArray(group_stack);
   1993 	pushG(group_stack, group);
   1994 	forEachSmallArray(text_array, L) {
   1995 		castS(l, L);
   1996 		// count space indents
   1997 		i64 indent = 0;
   1998 		char *s    = ssGet(l);
   1999 		range(i, lenG(l)) {
   2000 			if (s[i] != ' ') {
   2001 				break;
   2002 			}
   2003 			indent++;
   2004 		}
   2005 		if (indent < lenG(group_stack)-1) {
   2006 			// remove groups from stack if same level or higher levels
   2007 			delG(group_stack, -(lenG(group_stack)-1 - indent), 0);
   2008 		}
   2009 		trimG(l);
   2010 		const char *tid = add_text(getG(group_stack, rtChar,-1), l);
   2011 		create_group(tid);
   2012 		pushG(group_stack, tid);
   2013 		number_of_tasks++;
   2014 		finishG(l);
   2015 	}
   2016 
   2017 	sprintf(return_add_many_groups_from_text_array, "created %ld groups in group %s", number_of_tasks, group);
   2018 	edi_log(return_add_many_groups_from_text_array);
   2019 	return return_add_many_groups_from_text_array;
   2020 }
   2021 
   2022 /* create group from text file
   2023  * @param[in] group task id
   2024  * @param[in] text_file with group structure
   2025  * @ingroup EDI_CORE
   2026  */
   2027 const char *add_many_groups_from_text(const char *group, const char *text_file) {
   2028 
   2029 	// load file and call add_many_groups_from_text_array
   2030 	createAllocateSmallArray(text_array);
   2031 	readFileG(text_array, text_file);
   2032 	const char *r = add_many_groups_from_text_array(group, text_array);
   2033 	terminateG(text_array);
   2034 
   2035 	return r;
   2036 }
   2037 
   2038 /** string returned from export_task_to_a_file */
   2039 static char return_export_task_to_a_file[8192];
   2040 
   2041 /** copy description to path using first line of description as filename
   2042  * @return path and filname
   2043  * @param[in] tid task id
   2044  * @param[in] path destination directory for task description
   2045  * @ingroup EDI_CORE
   2046  */
   2047 const char *export_task_to_a_file(const char *tid, const char *path) {
   2048 	createTaskPath(dPath, "description.txt");
   2049 	createAllocateSmallArray(des);
   2050 	readFileG(des, dPath);
   2051 	smallStringt *fn = getNDupG(des, rtSmallStringt, 0);
   2052 	trimG(fn);
   2053 	delG(des, 0, 1);
   2054 	sprintf(return_export_task_to_a_file, "%s/%s", path, ssGet(fn));
   2055 	// remove eventual double //
   2056 	uniqSlash((char *)&return_export_task_to_a_file);
   2057 
   2058 	writeFileG(des, return_export_task_to_a_file);
   2059 	return return_export_task_to_a_file;
   2060 }
   2061 
   2062 /** print description of task tid
   2063  * @return list of strings
   2064  * @param[in] tid task id
   2065  * @param[in] titleFunc Function that changes first description line
   2066  * @ingroup EDI_CORE
   2067  * In GUI, titleFunc (passThroughTitle function) keeps original title line.
   2068  */
   2069 // python call: def display_task tid, titleFunc=generate_task_string_with_tid
   2070 smallArrayt *display_task(const char *tid, smallStringt *titleFunc(const char *, smallStringt *)) {
   2071 	createTaskPath(task_path, "description.txt");
   2072 	createAllocateSmallArray(description);
   2073 	if (isBlankG(tid)) {
   2074 		// tid is invalid, return empty description
   2075 		pushG(description, "");
   2076 		return description;
   2077 	}
   2078 	readFileG(description, task_path);
   2079 
   2080 	// print tid, status and first line
   2081 	smallStringt *s = getG(description, rtSmallStringt, 0);
   2082 	if (s) {
   2083 		setNFreeG(description, 0, titleFunc(tid, s));
   2084 	}
   2085 	else {
   2086 		// empty description, push empty line
   2087 		pushG(description, "");
   2088 	}
   2089 	finishG(s);
   2090 	return description;
   2091 }
   2092 
   2093 
   2094 /** string returned frm find_group_containing_task */
   2095 static char return_find_group_containing_task[64];
   2096 
   2097 /** find group containing task tid in groups folder
   2098  * @return tid
   2099  * @param[in] tid task id
   2100  * @ingroup EDI_CORE
   2101  */
   2102 const char *find_group_containing_task(const char *tid) {
   2103 	if (eqG(tid, "root")) {
   2104 		// root has not parent group, return root
   2105 		return tid;
   2106 	}
   2107 	return_find_group_containing_task[0] = 0;
   2108 	//#:define walk_groups
   2109 	// reuse previously created group list, to save time
   2110 	if (not lenG(group_directory_file_list)) {
   2111 		terminateG(group_directory_file_list);
   2112 ;		group_directory_file_list = walkDirG(rtSmallArrayt, data_location_groups);
   2113 	}
   2114 	smallArrayt *groups_and_tasks = group_directory_file_list;
   2115 	forEachSmallArray(groups_and_tasks, GT) {
   2116 		castS(t, GT);
   2117 		smallArrayt *ts = splitG(t, "/");
   2118 		if (endsWithG(t, tid) and not eqG(tid, getG(ts, rtChar, -2))) {
   2119 			strcpy(return_find_group_containing_task,  getG(ts, rtChar, -2));
   2120 		}
   2121 		terminateG(ts);
   2122 		finishG(GT);
   2123 	}
   2124 	//#:end
   2125 	if (return_find_group_containing_task[0] == 0) {
   2126 		printf("\n\nEDI_ERROR: Database inconsistent - run: find %s|grep %s and remove reference.\n", data_location, tid);
   2127 		strcpy(return_find_group_containing_task, "error");
   2128 	}
   2129 	return return_find_group_containing_task;
   2130 }
   2131 
   2132 /** find group containing task tid in groups folder and print
   2133  * @return list of strings
   2134  * @param[in] tid task id
   2135  * @ingroup EDI_CORE
   2136  * Shows path in tree and title for each group in path
   2137  */
   2138 smallArrayt *show_group_for_task(const char *tid) {
   2139 	createAllocateSmallArray(r);
   2140 
   2141 	// set group string to be displayed on first line
   2142 	char *group_s = "     ";
   2143 	if (is_this_task_a_group(tid)) {
   2144 		group_s = "GROUP";
   2145 	}
   2146 	if (is_linked(tid)) {
   2147 		group_s = " LINK";
   2148 	}
   2149 
   2150 	//#:walk_groups
   2151 	// reuse previously created group list, to save time
   2152 	if (not lenG(group_directory_file_list)) {
   2153 		terminateG(group_directory_file_list);
   2154 		group_directory_file_list = walkDirG(rtSmallArrayt, data_location_groups);
   2155 	}
   2156 	smallArrayt *groups_and_tasks = group_directory_file_list;
   2157 	createAllocateSmallString(treePath);
   2158 	forEachSmallArray(groups_and_tasks, GT) {
   2159 		castS(t, GT);
   2160 		smallArrayt *ts = splitG(t, "/");
   2161 		if (endsWithG(t, tid) and not eqG(tid, getG(ts, rtChar, -2))) {
   2162 			char *group = return_find_group_containing_task;
   2163 			strcpy(return_find_group_containing_task,  getG(ts, rtChar, -2));
   2164 			const char *tree_path = find_group_in_tree(group);
   2165 
   2166 			// p is data_location folder/tree
   2167 			smallStringt *s = allocG(data_location_tree);
   2168 			smallArrayt *p_l = splitG(s, "/");
   2169 			terminateG(s);
   2170 			sliceG(p_l, -2, 0);
   2171 			smallStringt *p  = joinG(p_l, "/");
   2172 			terminateG(p_l);
   2173 
   2174 			// if empty then command is run in tree root
   2175 			setFromG(treePath, tree_path);
   2176 			p_l = splitG(treePath, ssGet(p));
   2177 			if (lenG(getG(p_l, rtChar, -1))) {
   2178 				// print path of tids: tid/tid...
   2179 				s = getNDupG(p_l, rtSmallStringt, -1);
   2180 				// remove /
   2181 				delG(s, 0, 1);
   2182 				pushG(r,  s);
   2183 				smallArrayt *g_l = splitG(s, "/");
   2184 				finishG(s);
   2185 				createAllocateSmallArray(group_titles_in_path);
   2186 				forEachSmallArray(g_l, G) {
   2187 					castS(g, G);
   2188 					pushNFreeG(group_titles_in_path, get_task_title(ssGet(g)));
   2189 					finishG(g);
   2190 				}
   2191 				terminateG(g_l);
   2192 				// print title/title...
   2193 				pushNFreeG(r, joinG(group_titles_in_path, "/"));
   2194 				terminateG(group_titles_in_path);
   2195 			}
   2196 			terminateManyG(p, p_l);
   2197 			p = get_task_title(group);
   2198 			pushNFreeG(r, generate_group_string_with_tid(group, p));
   2199 			terminateG(p);
   2200 			pushG(r, "");
   2201 		}
   2202 		terminateG(ts);
   2203 		finishG(GT);
   2204 	}
   2205 	terminateG(treePath);
   2206 
   2207 	// Print media and attachments
   2208 	// media type
   2209 	smallArrayt *m           = get_media(tid);
   2210 	char *media              = getG(m, rtChar, 0);
   2211 	// attachment list
   2212 	smallArrayt *attachments = get_attachments(tid);
   2213 
   2214 	// Print task, colors, group list
   2215 	smallArrayt *color       = get_forground_color(tid);
   2216 	char *fc                 = toStringG(color);
   2217 	terminateG(color);
   2218 	color                    = get_background_color(tid);
   2219 	char *bc                 = toStringG(color);
   2220 	// Figure out creation time and modification times for description and status
   2221 	smallArrayt *tctime      = get_creation_date(tid);
   2222 	char *task_ctime         = getG(tctime, rtChar, 0);
   2223 
   2224 	createTaskPath(task_path, "description.txt");
   2225 	char *description_mtime = timeToS(getModificationTimeG(task_path));
   2226 	genTaskPath(task_path, "status");
   2227 	char *status_mtime      = timeToS(getModificationTimeG(task_path));
   2228 	smallStringt *tt        = get_task_title(tid);
   2229 	prependG(tt, " ");
   2230 	prependG(tt, group_s);
   2231 	smallStringt *s         = generate_task_string_with_tid(tid, tt);
   2232 	createAllocateSmallArray(info);
   2233 	pushG                   (info, "Tasks:");
   2234 	pushG                   (info, ssGet(s));
   2235 	pushG                   (info, "");
   2236 	pushG                   (info, "");
   2237 	terminateManyG(tt, s);
   2238 	pushNFreeG              (info, appendG("Media type: ", media));
   2239 	pushG                   (info, "");
   2240 	pushG                   (info, "Attachments:");
   2241 	appendNSmashG           (info, attachments);
   2242 	pushG                   (info, "");
   2243 	pushG                   (info, "");
   2244 	pushNFreeG              (info, formatS("foreground color:        %s", fc));
   2245 	pushNFreeG              (info, formatS("background color:        %s", bc));
   2246 	pushNFreeG              (info, formatS("Creation time:           %s", task_ctime));
   2247 	pushNFreeG              (info, formatS("Last description change: %s", description_mtime));
   2248 	pushNFreeG              (info, formatS("Last status change:      %s", status_mtime));
   2249 	pushG                   (info, "");
   2250 	pushG                   (info, "Group list");
   2251 	appendNSmashG           (info, r);
   2252 	r = info;
   2253 	terminateManyG(m, color, tctime);
   2254 	freeManyS(fc, bc, description_mtime, status_mtime);
   2255 	return r;
   2256 }
   2257 
   2258 /** string returned from find_group_in_tree */
   2259 static char return_find_group_in_tree[8192];
   2260 
   2261 /** find group in tree folder
   2262  * @return path_in_tree
   2263  * @param[in] tid task id
   2264  * @ingroup EDI_CORE
   2265  */
   2266 const char *find_group_in_tree(const char *group) {
   2267 	char *path_in_tree;
   2268 	if (eqG(group, "root")) {
   2269 		path_in_tree = data_location_tree;
   2270 	}
   2271 	else {
   2272 		path_in_tree = return_find_group_in_tree;
   2273 		path_in_tree[0] = 0;
   2274 
   2275 		// list all group paths in tree
   2276 		/* if platform.system() = 'Windows' */
   2277 		/* 	folders = [] */
   2278 		/* 	folders append data_location_tree */
   2279 		/* 	for dir, subdirs, files in os.walk(data_location_tree) */
   2280 		/* 		for sd in subdirs */
   2281 		/* 			folders append dir+os.sep + sd */
   2282 		/* else */
   2283 		smallArrayt *folders = walkDirAllG(rtSmallArrayt, data_location_tree);
   2284 		enumerateSmallArray(folders, F, i) {
   2285 			castS(f, F);
   2286 			trimG(f);
   2287 			setNFreePG(folders, i, f);
   2288 		}
   2289 		smallArrayt *groups       = folders;
   2290 		// find the group in group paths
   2291 		forEachSmallArray(groups, G) {
   2292 			castS(g, G);
   2293 			char *s = basename(ssGet(g));
   2294 			if (eqG(s, group))
   2295 				strcpy(path_in_tree, ssGet(g));
   2296 			finishG(g);
   2297 		}
   2298 		terminateG(folders);
   2299 	}
   2300 	return path_in_tree;
   2301 }
   2302 
   2303 /** Determines if a task has multiple references
   2304  * @return integer 0 or 1
   2305  * @param[in] tid task id
   2306  * @ingroup EDI_CORE
   2307  */
   2308 bool is_linked(const char *tid) {
   2309 	bool status = false;
   2310 	createTaskPath(task_linked_groups_path, "groups/");
   2311 	// check if tid/groups exists
   2312 	if (fileExists(task_linked_groups_path)) {
   2313 		smallArrayt *groups = readDirG(rtSmallArrayt, task_linked_groups_path);
   2314 		// check if task is linked to more than 1 group
   2315 		if (lenG(groups) > 1) {
   2316 			status = true;
   2317 		}
   2318 		terminateG(groups);
   2319 	}
   2320 	return status;
   2321 }
   2322 
   2323 /** convert task to group
   2324  * @return list of stings when there is an error
   2325  * @param[in] tid task id
   2326  * @ingroup EDI_CORE
   2327  */
   2328 smallArrayt *create_group(const char *tid) {
   2329 	createAllocateSmallArray(r);
   2330 	// convert task to group only when task is not linked
   2331 	if (is_linked(tid)) {
   2332 		pushG(r, "Converting linked task to group removes links. The task groups are:");
   2333 		appendNSmashG(r, show_group_for_task(tid));
   2334 
   2335 		// delete tid/groups because groups are not linked
   2336 		createTaskPath(task_linked_groups_path, "groups/");
   2337 		smallArrayt *groups = readDirG(rtSmallArrayt, task_linked_groups_path);
   2338 		// remove all links except for the first group
   2339 		forEachSmallArray(groups, G) {
   2340 			castS(g, G);
   2341 			delete_linked_task(ssGet(g),tid);
   2342 			finishG(g);
   2343 		}
   2344 		if (fileExistsG(task_linked_groups_path)) {
   2345 			rmAll(task_linked_groups_path);
   2346 		}
   2347 		pushNFreeG(r, formatS("Created group %s in %s", tid, getG(groups, rtChar,0)));
   2348 		terminateG(groups);
   2349 	}
   2350 	// create new group in groups folder
   2351 	char p[8192];
   2352 	sprintf(p, "%s/%s", data_location_groups, tid);
   2353 	mkdirParentsG(p);
   2354 	// First task in group is group title task
   2355 	const char *order_id = baseconvert(0);
   2356 	sprintf(p, "%s/%s%s", generate_group_path(tid), order_id, tid);
   2357 	FILE *f              = fopen(p, "w");
   2358 	fclose(f);
   2359 
   2360 	// Add group in tree
   2361 	// update group list
   2362 	emptyG(group_directory_file_list);
   2363 	const char *group    = find_group_containing_task(tid);
   2364 	if (eqG(group, "root")) {
   2365 		sprintf(p, "%s/%s", data_location_tree, tid);
   2366 		mkdirParentsG(p);
   2367 		// git support
   2368 		sprintf(p, "%s/%s/.keepDir", data_location_tree, tid);
   2369 		f = fopen(p, "w");
   2370 		fclose(f);
   2371 	}
   2372 	else {
   2373 		const char *s = find_group_in_tree(group);
   2374 		sprintf(p, "%s/%s", s, tid);
   2375 		mkdirParentsG(p);
   2376 		// git support
   2377 		sprintf(p, "%s/%s/.keepDir", s, tid);
   2378 		f = fopen(p, "w");
   2379 		fclose(f);
   2380 	}
   2381 
   2382 	// Change status active to void by default
   2383 	// To avoid filtering groups
   2384 	if (eqG(get_status(tid), TASK_STATUS[TASK_STATUS_ACTIVE])) {
   2385 		// set status to void
   2386 		set_status(tid,TASK_STATUS_VOID);
   2387 	}
   2388 	sprintf(p, "created group %s", tid);
   2389 	edi_log(p);
   2390 	return r;
   2391 }
   2392 
   2393 /** string returned from convert_group_to_task */
   2394 static char return_convert_group_to_task[1];
   2395 
   2396 /** convert group to task
   2397  * @param[in] group group to convert to task
   2398  * @ingroup EDI_CORE
   2399  */
   2400 const char *convert_group_to_task(const char *group) {
   2401 	char *r = return_convert_group_to_task;
   2402 	r[0]    = 0;
   2403 
   2404 	// list all tasks (data_location_groups/GROUP/'ORDER_ID''TASK_ID') in groups folder
   2405 	smallArrayt *groups_and_tasks = walkDirG(rtSmallArrayt, data_location_groups);
   2406 
   2407 	char *convert_group_status = "delete";
   2408 	char groupslash[ORDER_ID_LENGTH + ID_LENGTH + 1];
   2409 	sprintf(groupslash, "%s/", group);
   2410 	forEachSmallArray(groups_and_tasks, T2) {
   2411 		castS(t2, T2);
   2412 		// search a task in group that is not tid
   2413 		char *t2task = basename(ssGet(t2));
   2414 		if (hasG(t2, groupslash) and (not eqG(group, t2task)))
   2415 			convert_group_status = "There is another task in the group, keeping group.";
   2416 			r = convert_group_status;
   2417 			// just need to find one task
   2418 			break;
   2419 		finishG(t2);
   2420 	}
   2421 	terminateG(groups_and_tasks);
   2422 	if (eqG(convert_group_status, "delete") and (not hasG(group, "root"))) {
   2423 		// Delete group title and group folder
   2424 		rmAllG(generate_group_path(group));
   2425 		// Delete group in tree
   2426 		const char *treepath = find_group_in_tree(group);
   2427 		if (not isEmptyG(treepath)) {
   2428 			rmAll(treepath);
   2429 		}
   2430 		// change state from void to active
   2431 		set_status(group,TASK_STATUS_ACTIVE);
   2432 		char log[100];
   2433 		sprintf(log, "converted group %s to task", group);
   2434 		edi_log(log);
   2435 	}
   2436 	return r;
   2437 }
   2438 
   2439 /** string returned from delete_task */
   2440 static char return_delete_task[ORDER_ID_LENGTH + ID_LENGTH + 1];
   2441 
   2442 /** delete task tid in all groups
   2443  * @return group id
   2444  * @param[in] tid task id
   2445  * @ingroup EDI_CORE
   2446  */
   2447 const char *delete_task(const char *tid) {
   2448 	// Delete task in tasks
   2449 	rmAll(generate_task_path(tid));
   2450 
   2451 	// Delete task reference in groups
   2452 	// list all tasks (data_location_groups/GROUP/'ORDER_ID''TASK_ID') in groups folder
   2453 	smallArrayt *groups_and_tasks = walkDirG(rtSmallArrayt, data_location_groups);
   2454 
   2455 	// Identify if task is a group
   2456 	bool is_a_group = is_this_task_a_group(tid);
   2457 
   2458 	// Return group task, it changes when first task is deleted
   2459 	char *group_id  = return_delete_task;
   2460 	group_id[0]     = 0;
   2461 	// list groups to reorder at the end
   2462 	createAllocateSmallArray(reorder_groups);
   2463 	forEachSmallArray(groups_and_tasks, T) {
   2464 		castS(t, T);
   2465 		// Delete reference in upper group, not the title task of the group
   2466 		smallArrayt *ta = splitG(t, "/");
   2467 		char thisGroup[8192];
   2468 		strcpy(thisGroup, getG(ta, rtChar, -2));
   2469 		terminateG(ta);
   2470 		if ((endsWithG(t, tid)) and (not eqG(tid, thisGroup))) {
   2471 			// group is the group for task tid
   2472 			strcpy(group_id, thisGroup);
   2473 			// Delete task reference in group
   2474 			rmAllG(t);
   2475 			pushG(reorder_groups, group_id);
   2476 			if (is_a_group) {
   2477 				// First task becomes a group, add a reference in group group
   2478 				// list tasks in order in tid group
   2479 				smallArrayt *group_tasks = readDirG(rtSmallArrayt, generate_group_path(tid));
   2480 				// Delete emtpy group or first task in group becomes the group title.
   2481 				if (lenG(group_tasks) == 1) {
   2482 					rmAllG(generate_group_path(tid));
   2483 					// Delete group in tree
   2484 					// when a group is deleted, subgroups are automatically deleted in the tree
   2485 					const char *s = find_group_in_tree(tid);
   2486 					if (not isEmptyG(s)) {
   2487 						rmAllG(s);
   2488 					}
   2489 				}
   2490 				else {
   2491 					// Remove order_id, keep task id only
   2492 					char *first_task  = getG(group_tasks, rtChar,1) + ORDER_ID_LENGTH;
   2493 					strcpy(group_id, first_task);
   2494 					// Create an entry in group at the same position as tid had
   2495 					ta = splitG(t, "/");
   2496 					char *order_id = copyRngG(getG(ta, rtChar, -1), 0, ORDER_ID_LENGTH);
   2497 					terminateG(ta);
   2498 					char groupPath[8192];
   2499 					sprintf(groupPath, "%s/%s%s", generate_group_path(group_id), order_id, first_task);
   2500 					free(order_id);
   2501 					FILE *f = fopen(groupPath, "w");
   2502 					fclose(f);
   2503 
   2504 					// Delete group task of group of more then 2 tasks, first task becomes a group
   2505 					// delete orderidtaskid
   2506 					sprintf(groupPath, "%s/%s", generate_group_path(tid), getG(group_tasks, rtChar, 0));
   2507 					rmAllG(groupPath);
   2508 					const char *path = generate_group_path(tid);
   2509 					char src[8192];
   2510 					sprintf(src, "%s/%s", path, getG(group_tasks, rtChar, 1));
   2511 					char dst[8192];
   2512 					sprintf(dst, "%s/%s%s", path, baseconvert(0), first_task);
   2513 					shRename(src, dst);
   2514 					// reorder tasks to remove gap between group title task and first task
   2515 					smallArrayt *tasks = readDirG(rtSmallArrayt, path);
   2516 					enumerateSmallArray(tasks, T, n) {
   2517 						castS(t, T);
   2518 						char src[8192];
   2519 						sprintf(src, "%s/%s", path, ssGet(t));
   2520 						char dst[8192];
   2521 						sprintf(dst, "%s/%s%s", path, baseconvert(n), ssGet(t)+ORDER_ID_LENGTH);
   2522 						shRename(src, dst);
   2523 						finishG(t);
   2524 					}
   2525 					terminateG(tasks);
   2526 					// rename group in groups folder
   2527 					shRename(path, generate_group_path(first_task));
   2528 					// rename group in tree folder
   2529 					const char *group_tree_path       = find_group_in_tree(tid);
   2530 					smallStringt *s                   = allocG(group_tree_path);
   2531 					smallArrayt *group_tree_path_l    = splitG(s, "/");
   2532 					terminateG(s);
   2533 					setG(group_tree_path_l, -1, first_task);
   2534 					smallStringt *new_group_tree_path = joinG(group_tree_path_l, "/");
   2535 					shRename(group_tree_path, ssGet(new_group_tree_path));
   2536 					terminateManyG(new_group_tree_path, group_tree_path_l);
   2537 				}
   2538 				terminateG(group_tasks);
   2539 			}
   2540 		}
   2541 		finishG(t);
   2542 	}
   2543 	terminateG(groups_and_tasks);
   2544 
   2545 	// reorder tasks to remove gaps in reorder_groups
   2546 	forEachSmallArray(reorder_groups, T) {
   2547 		castS(tidg, T);
   2548 		const char *path = generate_group_path(ssGet(tidg));
   2549 		smallArrayt *tasks = readDirG(rtSmallArrayt, path);
   2550 		enumerateSmallArray(tasks, TD, n) {
   2551 			castS(t, TD);
   2552 			char src[8192];
   2553 			sprintf(src, "%s/%s", path, ssGet(t));
   2554 			char dst[8192];
   2555 			sprintf(dst, "%s/%s%s", path, baseconvert(n), ssGet(t)+ORDER_ID_LENGTH);
   2556 			shRename(src, dst);
   2557 			finishG(t);
   2558 		}
   2559 		terminateG(tasks);
   2560 		finishG(tidg);
   2561 	}
   2562 
   2563 	// Return group task
   2564 	smallStringt *s = joinG(reorder_groups, " ");
   2565 	char *log = formatS("deleted %s in %s", tid, ssGet(s));
   2566 	edi_log(log);
   2567 	free(log);
   2568 	terminateManyG(reorder_groups, s);
   2569 	return group_id;
   2570 }
   2571 
   2572 /** string returned from delete_linked_task */
   2573 static char return_delete_linked_task[ORDER_ID_LENGTH + ID_LENGTH + 1];
   2574 
   2575 /** Delete task only if it is linked in one group
   2576  * @return group id
   2577  * @param[in] group task id
   2578  * @param[in] tid task id
   2579  * @ingroup EDI_CORE
   2580  */
   2581 const char *delete_linked_task(const char *group, const char *tid) {
   2582 	char *group_id = return_delete_linked_task;
   2583 	strcpy(group_id, group);
   2584 
   2585 	if (not is_linked(tid)) {
   2586 		group_id = (char *) delete_task(tid);
   2587 	}
   2588 	else {
   2589 		// Delete task reference in group
   2590 		const char *path = generate_group_path(group);
   2591 		char p[8192];
   2592 
   2593 		// find task in group: ORDER_IDTASK_ID
   2594 		smallArrayt *tasks = readDirG(rtSmallArrayt, path);
   2595 		forEachSmallArray(tasks, T) {
   2596 			castS(t, T);
   2597 			if (eqG(ssGet(t)+ORDER_ID_LENGTH, tid)) {
   2598 				sprintf(p, "%s%s", path, ssGet(t));
   2599 				rmAllG(p);
   2600 			}
   2601 			finishG(t);
   2602 		}
   2603 		terminateG(tasks);
   2604 
   2605 		// delete group in tid/groups
   2606 		sprintf(p, "%s/groups/%s", generate_task_path(tid), group);
   2607 		rmAllG(p);
   2608 
   2609 		// reorder tasks to remove gaps in group
   2610 		tasks = readDirG(rtSmallArrayt, path);
   2611 		enumerateSmallArray(tasks, T, n) {
   2612 			castS(t, T);
   2613 			char src[8192];
   2614 			sprintf(src, "%s/%s", path, ssGet(t));
   2615 			char dst[8192];
   2616 			sprintf(dst, "%s/%s%s", path, baseconvert(n), ssGet(t)+ORDER_ID_LENGTH);
   2617 			shRename(src, dst);
   2618 			finishG(t);
   2619 		}
   2620 		terminateG(tasks);
   2621 
   2622 		sprintf(p, "deleted %s in group %s", tid, group);
   2623 		edi_log(p);
   2624 	}
   2625 
   2626 	return group_id;
   2627 }
   2628 
   2629 /** delete group tid
   2630  * @return group id
   2631  * @param[in] tid task id
   2632  * @ingroup EDI_CORE
   2633  */
   2634 const char *delete_group(const char *tid) {
   2635 	// Delete tasks in group
   2636 	smallArrayt *group_tasks = readDirG(rtSmallArrayt, generate_group_path(tid));
   2637 	if ((lenG(group_tasks) == 0) and eqG(tid, "root")) {
   2638 		// the database is already empty
   2639 		return tid;
   2640 	}
   2641 	if (not eqG(tid, "root")) {
   2642 		// root group doesnt have a title
   2643 		// Remove group title from the loop to delete tasks only and then group
   2644 		delG(group_tasks, 0, 1);
   2645 	}
   2646 
   2647 	// Delete tasks and groups recursively
   2648 	forEachSmallArray(group_tasks, T) {
   2649 		castS(t, T);
   2650 		// Remove order_id, keep task id only
   2651 		char *oid = ssGet(t)+ORDER_ID_LENGTH;
   2652 		if (not is_this_task_a_group(oid)) {
   2653 			// delete tasks that are linked only once
   2654 			delete_linked_task(tid, oid);
   2655 		}
   2656 		else {
   2657 			delete_group(oid);
   2658 		}
   2659 		finishG(t);
   2660 	}
   2661 	terminateG(group_tasks);
   2662 
   2663 	if (not eqG(tid, "root")) {
   2664 		// never delete root group
   2665 		return delete_task(tid);
   2666 	}
   2667 	else {
   2668 		// return root and keep root task
   2669 		return tid;
   2670 	}
   2671 }
   2672 
   2673 /** edit task with vi
   2674  * @param[in] tid task id
   2675  * @ingroup EDI_CORE
   2676  */
   2677 void edit_task(const char *tid) {
   2678 	createTaskPath(dPath, "description.txt");
   2679 	char c[24576];
   2680 	sprintf(c, "%s %s", editor, dPath);
   2681 	system(c);
   2682 	sprintf(c, "edited %s", tid);
   2683 	edi_log(c);
   2684 }
   2685 
   2686 /** move task from group at at_pos to to_pos and reorder
   2687  * @return list of stings when there is an error
   2688  * @param[in] group task id
   2689  * @param[in] at_pos selected position
   2690  * @param[in] to_pos insert position
   2691  * @ingroup EDI_CORE
   2692  */
   2693 smallArrayt *change_task_order(const char *group, i64 at_pos, i64 to_pos) {
   2694 	// List tasks in group
   2695 	createAllocateSmallArray(r);
   2696 	char path[8192];
   2697 	strcpy(path, generate_group_path(group));
   2698 	smallArrayt *tasks = readDirG(rtSmallArrayt, path);
   2699 
   2700 	// Verify position
   2701 	// Get task
   2702 	char *orderid_and_tid = getG(tasks, rtChar, at_pos);
   2703 	if (not orderid_and_tid) {
   2704 		pushNFreeG(r, formatS("%ld is an invalid position.", at_pos));
   2705 		goto end;
   2706 	}
   2707 	if (to_pos == 0) {
   2708 		char *tid = orderid_and_tid+ORDER_ID_LENGTH;
   2709 		// do not move linked tasks to position 0
   2710 		if (is_linked(tid)) {
   2711 			pushG(r, "Converting linked task to group removes links. The task groups are:");
   2712 			appendNSmashG(r, show_group_for_task(tid));
   2713 
   2714 			// delete tid/groups because groups are not linked
   2715 			createTaskPath(task_linked_groups_path, "groups/");
   2716 			smallArrayt *groups = readDirG(rtSmallArrayt, task_linked_groups_path);
   2717 			// remove all links except for the first group
   2718 			forEachSmallArray(groups, G) {
   2719 				castS(g, G);
   2720 				if (not eqG(g, group)) {
   2721 					delete_linked_task(ssGet(g),tid);
   2722 				}
   2723 				finishG(g);
   2724 			}
   2725 			terminateG(groups);
   2726 			if (fileExists(task_linked_groups_path)) {
   2727 				rmAll(task_linked_groups_path);
   2728 			}
   2729 			const char *parent_group = find_group_containing_task(group);
   2730 			pushNFreeG(r, formatS("Created group %s in %s",tid,parent_group));
   2731 		}
   2732 		// do not move groups to position 0
   2733 		if (is_this_task_a_group(tid)) {
   2734 			pushG(r, "Having a group in group title is not supported.");
   2735 			goto end;
   2736 		}
   2737 	}
   2738 	// Insert task at to_pos
   2739 	char *to_pos_tid = NULL;
   2740 	if (to_pos > at_pos) {
   2741 		// +1 because at_pos reference will be deleted and will shift to_pos reference
   2742 		to_pos += 1;
   2743 		injectSG(tasks, to_pos, orderid_and_tid);
   2744 		// Delete task at at_pos
   2745 		delG(tasks, at_pos, at_pos+1);
   2746 	}
   2747 	else {
   2748 		// rename group title, when to_pos in 0
   2749 		if ((to_pos == 0) and (not eqG(group, "root"))) {
   2750 			to_pos_tid = getG(tasks, rtChar, to_pos)+ORDER_ID_LENGTH;
   2751 		}
   2752 
   2753 		injectSG(tasks, to_pos, orderid_and_tid);
   2754 		// Delete task at at_pos+1 because the new position is before at_pos
   2755 		// at_pos was shifted when to_pos reference was added above
   2756 		delG(tasks, at_pos+1, at_pos+2);
   2757 	}
   2758 
   2759 	//:define reorder_tasks
   2760 	// Move tasks
   2761 	enumerateSmallArray(tasks, T, n) {
   2762 		castS(t, T);
   2763 		char src[8192];
   2764 		sprintf(src, "%s/%s", path, ssGet(t));
   2765 		char dst[8192];
   2766 		sprintf(dst, "%s/%s%s", path, baseconvert(n), ssGet(t)+ORDER_ID_LENGTH);
   2767 		shRename(src, dst);
   2768 		finishG(t);
   2769 	}
   2770 	//:end
   2771 
   2772 	// rename group title, when to_pos in 0
   2773 	if ((to_pos == 0) and (not eqG(group, "root"))) {
   2774 		// rename group in parent group
   2775 		// group is tid for parent group and to_pos_tid is equal to group input parameter because to_pos is 0
   2776 		const char *groupF = find_group_containing_task(group);
   2777 		// to_pos_orderid_and_tid is group in parent group
   2778 		smallStringt *to_pos_orderid_and_tid;
   2779 		smallArrayt *parent_group_tasks = readDirG(rtSmallArrayt, generate_group_path(groupF));
   2780 		forEachSmallArray(parent_group_tasks, T) {
   2781 			castS(t, T);
   2782 			if (eqG(ssGet(t)+ORDER_ID_LENGTH, to_pos_tid)) {
   2783 				to_pos_orderid_and_tid = dupG(t);
   2784 			}
   2785 			finishG(t);
   2786 		}
   2787 		terminateG(parent_group_tasks);
   2788 		// remove order_id, keep task id only for new group
   2789 		char *group_id         = orderid_and_tid+ORDER_ID_LENGTH;
   2790 		// create an entry in parent group at the same position as to_pos tid had
   2791 		smallStringt *order_id = copyRngG(to_pos_orderid_and_tid, 0, ORDER_ID_LENGTH);
   2792 		char p[8192];
   2793 		sprintf(p, "%s/%s%s", generate_group_path(groupF), order_id,group_id);
   2794 		FILE *f                = fopen(p, "w");
   2795 		fclose(f);
   2796 
   2797 		// delete group task of group
   2798 		// delete orderidtaskid
   2799 		sprintf(p, "%s/%s", generate_group_path(groupF), ssGet(to_pos_orderid_and_tid));
   2800 		rmAllG(p);
   2801 
   2802 		// rename group in groups folder
   2803 		strcpy(p, generate_group_path(to_pos_tid));
   2804 		shRename(p, generate_group_path(group_id));
   2805 		// rename group in tree folder
   2806 		const char *group_tree_path    = find_group_in_tree(to_pos_tid);
   2807 		char **group_tree_path_l       = splitG(group_tree_path, "/");
   2808 		setG(group_tree_path_l, -1, group_id);
   2809 		char *new_group_tree_path      = joinG(group_tree_path_l, "/");
   2810 		freeG(group_tree_path_l);
   2811 		shRename(group_tree_path, new_group_tree_path);
   2812 		free(new_group_tree_path);
   2813 
   2814 		// rename group in linked task and former linked task with a 'groups' folder in tasks database folder
   2815 		smallArrayt *group_tasks = readDirG(rtSmallArrayt, generate_group_path(group_id));
   2816 		forEachSmallArray(group_tasks, G) {
   2817 			castS(group_task, G);
   2818 			char tPath[8192];
   2819 			sprintf(tPath, "%s/groups", generate_task_path(ssGet(group_task)+ORDER_ID_LENGTH));
   2820 			if (fileExists(tPath)) {
   2821 				// search for to_pos_tid (group input parameter) in task path groups folder
   2822 				smallArrayt *link_groups = readDirG(rtSmallArrayt, tPath);
   2823 				forEachSmallArray(link_groups, LG) {
   2824 					castS(lg, LG);
   2825 					if (eqG(to_pos_tid, lg)) {
   2826 						// rename group to new group id since the title changed
   2827 						char src[8192];
   2828 						sprintf(src, "%s/%s", tPath, to_pos_tid);
   2829 						char dst[8192];
   2830 						sprintf(dst, "%s/%s", tPath, group_id);
   2831 						shRename(src, dst);
   2832 					}
   2833 					finishG(lg);
   2834 				}
   2835 				terminateG(link_groups);
   2836 			}
   2837 			finishG(G);
   2838 		}
   2839 		terminateG(group_tasks);
   2840 	}
   2841 
   2842 	char log[2048];
   2843 	sprintf(log, "changed task order %s to %s in group %s", at_pos, to_pos, group);
   2844 	edi_log(log);
   2845 end:
   2846 	terminateG(tasks);
   2847 	return r;
   2848 }
   2849 
   2850 /** move selected tasks in Easydoneit desktop from group to to_pos and reorder
   2851  * @return list of stings when there is an error
   2852  * @param[in] group task id
   2853  * @param[in] selected_tasks selected tasks in GUI list (array of ints)
   2854  * @param[in] to_pos insert position
   2855  * @ingroup EDI_CORE
   2856  */
   2857 void change_task_order_desktop(const char *group,smallArrayt *selected_tasks, i64 to_pos) {
   2858 	// List tasks in group
   2859 	const char *path   = generate_group_path(group);
   2860 	smallArrayt *tasks = readDirG(rtSmallArrayt, path);
   2861 
   2862 	createAllocateSmallArray(orderid_and_tids);
   2863 	forEachSmallArray(selected_tasks, T) {
   2864 		cast(smallIntt *, t, T);
   2865 		pushG(orderid_and_tids, getG(tasks, rtChar, getG(t, rtI64, unusedV)));
   2866 		finishG(t);
   2867 	}
   2868 	sortG(orderid_and_tids);
   2869 
   2870 	smallArrayt *task1 = copyRngG(tasks, 0, to_pos);
   2871 	smallArrayt *task2 = copyRngG(tasks, to_pos, 0);
   2872 
   2873 	// remove selected tasks from group list
   2874 	createAllocateSmallArray(task1m);
   2875 	forEachSmallArray(task1, I) {
   2876 		castS(i, I);
   2877 		if (not binarySearchG(orderid_and_tids, ssGet(i))) {
   2878 			pushG(task1m, i);
   2879 		}
   2880 		finishG(i);
   2881 	}
   2882 	createAllocateSmallArray(task2m);
   2883 	forEachSmallArray(task2, I) {
   2884 		castS(i, I);
   2885 		if (not binarySearchG(orderid_and_tids, ssGet(i))) {
   2886 			pushG(task2m, i);
   2887 		}
   2888 		finishG(i);
   2889 	}
   2890 	terminateManyG(task1, task2);
   2891 
   2892 	emptyG(tasks);
   2893 	catG(tasks, task1m, orderid_and_tids, task2m);
   2894 	smashManyO(task1m, orderid_and_tids, task2m);
   2895 
   2896 	//:reorder_tasks
   2897 	// Move tasks
   2898 	{enumerateSmallArray(tasks, T, n) {
   2899 		castS(t, T);
   2900 		char src[8192];
   2901 		sprintf(src, "%s/%s", path, ssGet(t));
   2902 		char dst[8192];
   2903 		sprintf(dst, "%s/%s%s", path, baseconvert(n), ssGet(t)+ORDER_ID_LENGTH);
   2904 		shRename(src, dst);
   2905 		finishG(t);
   2906 	}}
   2907 	terminateG(tasks);
   2908 
   2909 	{enumerateSmallArray(orderid_and_tids, T, n) {
   2910 		castS(t, T);
   2911 		sliceG(t, ORDER_ID_LENGTH, 0);
   2912 		setPG(orderid_and_tids, n, t);
   2913 		finishG(t);
   2914 	}}
   2915 	smallStringt *log = joinG(orderid_and_tids, " ");
   2916 	terminateG(orderid_and_tids);
   2917 	char *s = formatS("changed task order (%s) to %ld in group %s", ssGet(log), to_pos, group);
   2918 	terminateG(log);
   2919 	edi_log(s);
   2920 	free(s);
   2921 }
   2922 
   2923 /** string returned from move_task_to_a_group */
   2924 static char return_move_task_to_a_group[2048];
   2925 
   2926 /** move task to group
   2927  * @return tid
   2928  * @param[in] tgroup task id, select a task in this group
   2929  * @param[in] tid task id
   2930  * @param[in] group task id, destination group
   2931  * @ingroup EDI_CORE
   2932  */
   2933 const char *move_task_to_a_group(const char *tgroup, const char *tid, const char *group) {
   2934 
   2935 	// find task tid in tgroup and remove task
   2936 	char path[8192];
   2937 	strcpy(path, generate_group_path(tgroup));
   2938 	smallArrayt *tasks = readDirG(rtSmallArrayt, path);
   2939 	char task_in_group[ORDER_ID_LENGTH + ID_LENGTH +1];
   2940 	task_in_group[0] = 0;
   2941 	enumerateSmallArray(tasks, T, n) {
   2942 		castS(t, T);
   2943 		// remove task from tasks to reorder tgroup
   2944 		if (eqG(ssGet(t)+ORDER_ID_LENGTH, tid)) {
   2945 			strcpy(task_in_group, ssGet(t));
   2946 			delG(tasks, n, n+1);
   2947 			break;
   2948 		}
   2949 		finishG(t);
   2950 	}
   2951 	if (isEmptyG(task_in_group)) {
   2952 		sprintf(return_move_task_to_a_group, "%s not found in %s", tid, tgroup);
   2953 		return return_move_task_to_a_group;
   2954 	}
   2955 
   2956 	// Move group in tree
   2957 	if (is_this_task_a_group(tid)) {
   2958 		char treePath[8192];
   2959 		strcpy(treePath, find_group_in_tree(group));
   2960 		char *treeTid = strdup(find_group_in_tree(tid));
   2961 		sliceG(&treeTid, 0, -ID_LENGTH-1);
   2962 		if (eqG(treeTid, treePath)) {
   2963 			// prevent moving a group on itself, in tree and moving group title position to parent group
   2964 			free(treeTid);
   2965 			return tid;
   2966 		}
   2967 		free(treeTid);
   2968 		shRename(find_group_in_tree(tid), treePath);
   2969 	}
   2970 
   2971 	// Remove task in source group
   2972 	sprintf(path, "%s/%s", path, task_in_group);
   2973 	rmAll(path);
   2974 
   2975 	//:reorder_tasks
   2976 	// Move tasks
   2977 	{enumerateSmallArray(tasks, T, n) {
   2978 		castS(t, T);
   2979 		char src[8192];
   2980 		sprintf(src, "%s/%s", path, ssGet(t));
   2981 		char dst[8192];
   2982 		sprintf(dst, "%s/%s%s", path, baseconvert(n), ssGet(t)+ORDER_ID_LENGTH);
   2983 		shRename(src, dst);
   2984 		finishG(t);
   2985 	}}
   2986 	terminateG(tasks);
   2987 
   2988 	// check that tid is not already linked in destination group
   2989 	if (is_linked(tid)) {
   2990 		createTaskPath(gPath, "groups/");
   2991 		smallArrayt *link_groups = readDirG(rtSmallArrayt, gPath);
   2992 		if (hasG(link_groups, group)) {
   2993 			// removed task reference from tgroup. Remove tgroup reference in task. There is already a tid reference in group, nothing more to do
   2994 			char p[8192];
   2995 			sprintf(p, "%s/%s", gPath, tgroup);
   2996 			rmAllG(p);
   2997 			terminateG(link_groups);
   2998 			return tid;
   2999 		}
   3000 		terminateG(link_groups);
   3001 	}
   3002 
   3003 	// Create reference in destination group
   3004 	add_task_to_group_folder(tid, group);
   3005 
   3006 	// linked tasks, remove source group and add destination group
   3007 	if (is_linked(tid)) {
   3008 		// remove source tgroup from tid/groups
   3009 		createTaskPath(gPath, "groups/");
   3010 		char p[8192];
   3011 		sprintf(p, "%s/%s", gPath, tgroup);
   3012 		rmAllG(p);
   3013 		sprintf(p, "%s/%s", gPath, group);
   3014 		FILE *f = fopen(p, "w");
   3015 		fclose(f);
   3016 	}
   3017 
   3018 	char log[256];
   3019 	sprintf(log, "moved %s in group %s to group %s", tid, tgroup, group);
   3020 	edi_log(log);
   3021 	return tid;
   3022 }
   3023 
   3024 /** string returned from copy_task_to_a_group */
   3025 static char return_copy_task_to_a_group[ORDER_ID_LENGTH + ID_LENGTH +1];
   3026 
   3027 /** copy task to group with new tid
   3028  * @return new tid
   3029  * @param[in] tid task id
   3030  * @param[in] group task id, destination group
   3031  * @ingroup EDI_CORE
   3032  */
   3033 const char *copy_task_to_a_group(const char *tid, const char *group) {
   3034 	// Generate new tid
   3035 	strcpy(return_copy_task_to_a_group, generate_id());
   3036 	char *newtid = return_copy_task_to_a_group;
   3037 	// Copy task in tasks
   3038 	char newtidPath[8192];
   3039 	strcpy(newtidPath, generate_task_path(newtid));
   3040 	// add / to copy files in task
   3041 	char *tPath = (char*) generate_task_path(tid);
   3042 	strcat(tPath, "/");
   3043 	copy(tPath, newtidPath);
   3044 	// delete tid/groups because new task is not linked
   3045 	char task_linked_groups_path[8192];
   3046 	sprintf(task_linked_groups_path, "%s/groups/", newtidPath);
   3047 	if (fileExistsG(task_linked_groups_path)) {
   3048 		rmAllG(task_linked_groups_path);
   3049 	}
   3050 	// Copy group in groups and in tree
   3051 	if (is_this_task_a_group(tid)) {
   3052 		// create new group
   3053 		copy(generate_group_path(tid), newtidPath);
   3054 		// Change group title task to newtid
   3055 		char tPath[8192];
   3056 		sprintf(tPath, "%s/%s%s", newtidPath, baseconvert(0), tid);
   3057 		char ntPath[8192];
   3058 		sprintf(tPath, "%s/%s%s", newtidPath, baseconvert(0), newtid);
   3059 		shRename(tPath, ntPath);
   3060 
   3061 		// delete tasks to be recreated with new tid
   3062 		char p[8192];
   3063 		smallArrayt *tasks = readDirG(rtSmallArrayt ,generate_group_path(newtid));
   3064 		forEachSmallArray(tasks, T) {
   3065 			castS(t, T);
   3066 			sprintf(p, "%s/%s", generate_group_path(newtid), ssGet(t));
   3067 			rmAllG(p);
   3068 			finishG(t);
   3069 		}
   3070 		terminateG(tasks);
   3071 
   3072 		// Add group in tree
   3073 		if (eqG(group, "root")) {
   3074 			sprintf(p, "%s/%s", data_location_tree, newtid);
   3075 			mkdirParentsG(p);
   3076 		}
   3077 		else {
   3078 			sprintf(p, "%s/%s", find_group_in_tree(group), newtid);
   3079 		}
   3080 		mkdirParentsG(p);
   3081 	}
   3082 
   3083 
   3084 	// Add reference in group
   3085 	// Create reference in destination group
   3086 	add_task_to_group_folder(newtid, group);
   3087 
   3088 	if (is_this_task_a_group(tid)) {
   3089 		// walk in group
   3090 		// list items in group
   3091 		smallArrayt *tasks = readDirG(rtSmallArrayt, generate_group_path(tid));
   3092 
   3093 		// add group found in first group
   3094 		forEachSmallArray(tasks, T) {
   3095 			castS(t, T);
   3096 			// Check tasks that are not title task in a group
   3097 			if (not eqG(ssGet(t) + ORDER_ID_LENGTH, tid))
   3098 				// copy_task_to_a_group recursively
   3099 				copy_task_to_a_group(ssGet(t) + ORDER_ID_LENGTH,newtid);
   3100 			finishG(t);
   3101 		}
   3102 		terminateG(tasks);
   3103 	}
   3104 
   3105 	sprintf(newtidPath, "copied %s to group %s, created %s", tid,group,newtid);
   3106 	edi_log(newtidPath);
   3107 	return newtid;
   3108 }
   3109 
   3110 /** string returned from generate_task_path_in_database */
   3111 static char return_generate_task_path_in_database[8192];
   3112 
   3113 /** create task path in database using database name
   3114  * @return path to task in tasks
   3115  * @param[in] tid task id
   3116  * @param[in] location database name in data section of easydoneit.ini
   3117  * @ingroup EDI_CORE
   3118  */
   3119 const char *generate_task_path_in_database(const char *tid, const char *location) {
   3120 	char *locPath = getG(selected_d, rtChar, location);
   3121 	sprintf(return_generate_task_path_in_database, "%s/tasks/%s/", locPath, tid);
   3122 	return return_generate_task_path_in_database;
   3123 }
   3124 
   3125 /** string returned from generate_group_path_in_database */
   3126 static char return_generate_group_path_in_database[8192];
   3127 
   3128 /** create group path in database using database name
   3129  * @return path to group in groups
   3130  * @param[in] tid task id
   3131  * @param[in] location database name in data section of easydoneit.ini
   3132  * @ingroup EDI_CORE
   3133  */
   3134 const char *generate_group_path_in_database(const char *tid, const char *location) {
   3135 	char *locPath = getG(selected_d, rtChar, location);
   3136 	sprintf(return_generate_group_path_in_database, "%s/groups/%s/", locPath, tid);
   3137 	return return_generate_group_path_in_database;
   3138 }
   3139 
   3140 /** string returned from find_group_in_tree_in_database */
   3141 static char return_find_group_in_tree_in_database[8192];
   3142 
   3143 /** find group in tree folder in database
   3144  * @return path to group in tree
   3145  * @param[in] group task id
   3146  * @param[in] location database name in data section of easydoneit.ini
   3147  * @ingroup EDI_CORE
   3148  */
   3149 const char *find_group_in_tree_in_database(const char *group, const char *location) {
   3150 	char location_tree[8192];
   3151 	char *locPath = getG(selected_d, rtChar, location);
   3152 	sprintf(location_tree, "%s/tree", locPath);
   3153 	char *path_in_tree = return_find_group_in_tree_in_database;
   3154 	path_in_tree[0]    = 0;
   3155 
   3156 	if (eqG(group, "root")) {
   3157 		strcpy(return_find_group_in_tree_in_database, location_tree);
   3158 	}
   3159 	else {
   3160 		// list all group paths in tree
   3161 		/* if platform.system() = 'Windows' */
   3162 		/* 	folders = [] */
   3163 		/* 	folders append location_tree */
   3164 		/* 	for dir, subdirs, files in os.walk(location_tree) */
   3165 		/* 		for sd in subdirs */
   3166 		/* 			folders append dir+os.sep + sd */
   3167 		/* else */
   3168 		smallArrayt *folders = walkDirAllG(rtSmallArrayt, location_tree);
   3169 		enumerateSmallArray(folders, F, i) {
   3170 			castS(f, F);
   3171 			trimG(f);
   3172 			setNFreePG(folders, i, f);
   3173 		}
   3174 		smallArrayt *groups       = folders;
   3175 		// find the group in group paths
   3176 		forEachSmallArray(groups, G) {
   3177 			castS(g, G);
   3178 			char *s = basename(ssGet(g));
   3179 			if (eqG(s, group))
   3180 				strcpy(path_in_tree, ssGet(g));
   3181 			finishG(g);
   3182 		}
   3183 		terminateG(folders);
   3184 	}
   3185 	return path_in_tree;
   3186 }
   3187 
   3188 /** add task with new tid in selected database
   3189  * @param[in] tid task id
   3190  * @param[in] group task id
   3191  * @param[in] location database name in data section of easydoneit.ini
   3192  * @ingroup EDI_CORE
   3193  */
   3194 void add_task_to_group_folder_in_database(const char *tid, const char *group, const char* location) {
   3195 	// Create an entry in group
   3196 	smallArrayt *tasks = readDirG(rtSmallArrayt, generate_group_path_in_database(group,location));
   3197 	// Add +1 to last order_id to have the task last in the list
   3198 	const char *order_id;
   3199 	if (lenG(tasks)) {
   3200 		smallStringt *s   = getG(tasks, rtSmallStringt, -1);
   3201 		smallStringt *pos = copyRngG(s, 0, ORDER_ID_LENGTH);
   3202 		finishG(s);
   3203 		order_id = baseconvert(baseconvert_to_dec(ssGet(pos))+1);
   3204 		terminateG(pos);
   3205 	}
   3206 	else {
   3207 		// start at 0 when group is empty
   3208 		order_id = baseconvert(0);
   3209 	}
   3210 	terminateG(tasks);
   3211 	char p[8192];
   3212 	sprintf(p, "%s/%s%s", generate_group_path_in_database(group,location), order_id, tid);
   3213 	FILE *f = fopen(p, "w");
   3214 	fclose(f);
   3215 }
   3216 
   3217 /** string returned from copy_task_to_database */
   3218 static char return_copy_task_to_database[ORDER_ID_LENGTH + ID_LENGTH +1];
   3219 
   3220 /** copy task to group in selected database with new tid
   3221  * @return new tid
   3222  * @param[in] tid task id
   3223  * @param[in] group task id
   3224  * @param[in] location database name in data section of easydoneit.ini
   3225  * @ingroup EDI_CORE
   3226  */
   3227 const char *copy_task_to_database(const char *tid, const char *location, const char *group) {
   3228 	// Generate new tid
   3229 	char *newtid;
   3230 	newtid = return_copy_task_to_database;
   3231 	strcpy(newtid, generate_id());
   3232 	// Copy task in tasks
   3233 	// add / to copy files in task
   3234 	char *tidPath = (char*) generate_task_path(tid);
   3235 	strcat(tidPath, "/");
   3236 	copy(tidPath,generate_task_path_in_database(newtid,location));
   3237 	// delete tid/groups because new task is not linked
   3238 	char task_linked_groups_path[8192];
   3239 	sprintf(task_linked_groups_path, "%s/groups/", generate_task_path_in_database(newtid,location));
   3240 	if (fileExists(task_linked_groups_path)) {
   3241 		rmAllG(task_linked_groups_path);
   3242 	}
   3243 	// Copy group in groups and in tree
   3244 	if (is_this_task_a_group(tid)) {
   3245 		// create new group
   3246 		// add / to copy files in task
   3247 		tidPath = (char*) generate_task_path(tid);
   3248 		strcat(tidPath, "/");
   3249 		copy(tidPath,generate_group_path_in_database(newtid,location));
   3250 		// Change group title task to newtid
   3251 		char tPath[8192];
   3252 		sprintf(tPath, "%s/%s%s", generate_group_path_in_database(newtid,location), baseconvert(0), tid);
   3253 		char ntPath[8192];
   3254 		sprintf(ntPath, "%s/%s%s", generate_group_path_in_database(newtid,location), baseconvert(0), newtid);
   3255 		shRename(tPath, ntPath);
   3256 
   3257 		// delete tasks to be recreated with new tid
   3258 		smallArrayt *tasks = readDirG(rtSmallArrayt, generate_group_path_in_database(newtid,location));
   3259 		forEachSmallArray(tasks, T) {
   3260 			castS(t, T);
   3261 			sprintf(tPath, "%s/%s", generate_group_path_in_database(newtid,location), ssGet(t));
   3262 			rmAll(tPath);
   3263 			finishG(t);
   3264 		}
   3265 		terminateG(tasks);
   3266 
   3267 		// Add group in tree
   3268 		char location_tree[8192];
   3269 		char *locPath = getG(selected_d, rtChar, location);
   3270 		sprintf(location_tree, "%s/tree", locPath);
   3271 		if (eqG(group, "root")) {
   3272 			sprintf(tPath, "%s/%s", location_tree, newtid);
   3273 			mkdirParentsG(tPath);
   3274 		}
   3275 		else {
   3276 			sprintf(tPath, "%s/%s", find_group_in_tree_in_database(group,location), newtid);
   3277 			mkdirParentsG(tPath);
   3278 		}
   3279 	}
   3280 
   3281 
   3282 	// Add reference in group
   3283 	// Create reference in destination group
   3284 	add_task_to_group_folder_in_database(newtid, group, location);
   3285 
   3286 	if (is_this_task_a_group(tid)) {
   3287 		// walk in group
   3288 		// list items in group
   3289 		smallArrayt *tasks = readDirG(rtSmallArrayt, generate_group_path(tid));
   3290 
   3291 		// add group found in first group
   3292 		forEachSmallArray(tasks, T) {
   3293 			castS(t, T);
   3294 			// Check tasks that are not title task in a group
   3295 			if (eqG(ssGet(t)+ORDER_ID_LENGTH, tid)) {
   3296 				// copy_task_to_database recursively
   3297 				copy_task_to_database(ssGet(t)+ORDER_ID_LENGTH,location,newtid);
   3298 			}
   3299 			finishG(t);
   3300 		}
   3301 		terminateG(tasks);
   3302 	}
   3303 	return newtid;
   3304 }
   3305 
   3306 /** move task to database/group
   3307  * @param[in] tgroup group id for task tid
   3308  * @param[in] tid task id
   3309  * @param[in] location database name in data section of easydoneit.ini
   3310  * @param[in] group destination group id
   3311  * @ingroup EDI_CORE
   3312  * copy and delete
   3313  */
   3314 const char *move_task_to_a_group_to_database(const char *tgroup, const char *tid, const char *location, const char *group) {
   3315 	const char *newtid = copy_task_to_database(tid,location,group);
   3316 	if (is_this_task_a_group(tid)) {
   3317 		delete_group(tid);
   3318 	}
   3319 	else {
   3320 		delete_linked_task(tgroup,tid);
   3321 	}
   3322 	return newtid;
   3323 }
   3324 
   3325 /** add reference to tid in group
   3326  * @param[in] tid task id
   3327  * @param[in] group destination group id
   3328  * @ingroup EDI_CORE
   3329  * Used in edi ln to link tasks
   3330  */
   3331 void add_task_reference_to_a_group(const char *tid, smallStringt *group) {
   3332 	char p[8192];
   3333 	// Check if selected item is a task
   3334 	if (not is_this_task_a_group(tid)) {
   3335 		// add group to task folder
   3336 		createTaskPath(task_path, "groups");
   3337 		FILE *f;
   3338 		if (not fileExists(task_path)) {
   3339 			mkdirParentsG(task_path);
   3340 
   3341 			// add first group when task is linked to tid/groups/
   3342 			const char *first_group = find_group_containing_task(tid);
   3343 			sprintf(p, "%s/%s", task_path, first_group);
   3344 			f = fopen(p, "w");
   3345 			fclose(f);
   3346 		}
   3347 		sprintf(p, "%s/%s", task_path, ssGet(group));
   3348 		f = fopen(p, "w");
   3349 		fclose(f);
   3350 
   3351 		// add task to group
   3352 		add_task_to_group_folder(tid, ssGet(group));
   3353 	}
   3354 
   3355 	sprintf(p, "linked %s to group %s", tid, ssGet(group));
   3356 	edi_log(p);
   3357 }
   3358 
   3359 /** set status for tid
   3360  * @param[in] tid task id
   3361  * @param[in] status_number index in edi_core.TASK_STATUS
   3362  * @ingroup EDI_CORE
   3363  */
   3364 void set_status(const char *tid, i16 status_number) {
   3365 	// Change status
   3366 	createTaskPath(tpath, "status");
   3367 	writeFileG(TASK_STATUS[status_number], tpath);
   3368 	sprintf(tpath, "set status for %s to %s", tid, TASK_STATUS_TRIM[status_number]);
   3369 	edi_log(tpath);
   3370 }
   3371 
   3372 /* ## set all tasks in group to active */
   3373 /* # @param[in] tid task id */
   3374 /* # @ingroup EDI_CORE */
   3375 /* def reset_group_status tid */
   3376 /* 	group_tasks = os.listdir(generate_group_path(tid)) */
   3377 /* 	for t in group_tasks */
   3378 /* 		if (not is_this_task_a_group(t[ORDER_ID_LENGTH:])) or (is_this_task_a_group(t[ORDER_ID_LENGTH:]) and (get_status(t[ORDER_ID_LENGTH:]) != TASK_STATUS[TASK_STATUS_VOID])) */
   3379 /* 			set_status(t[ORDER_ID_LENGTH:],TASK_STATUS_ACTIVE) */
   3380 /* 	edi_log('reset group %s'%tid) */
   3381 /*  */
   3382 /** search string in tasks folder
   3383  * @param[in] search query string
   3384  * @ingroup EDI_CORE
   3385  */
   3386 smallArrayt *search_string(const char *search){
   3387 	smallArrayt *tasks  = walkDirG(rtSmallArrayt, data_location_tasks);
   3388 
   3389 	// search in descriptions only
   3390 	createAllocateSmallArray(grep_r);
   3391 	forEachSmallArray(tasks, TID) {
   3392 		castS(tido, TID);
   3393 		if (not eqG(tido, "root")) {
   3394 			// search in visible tasks only (filter enable status)
   3395 			const char *task_status = get_status(ssGet(tido));
   3396 			if (getG(status_filters_d, rtI32, task_status) == ENABLE) {
   3397 				/* if platform.system() = 'Windows' */
   3398 				/* 	// in windows, grep in python */
   3399 				/* 	f          = open generate_task_path(tid)+os.sep+'description.txt' */
   3400 				/* 	f_lines    = f readlines */
   3401 				/* 	f close */
   3402 				/* 	grep_lines = [] */
   3403 				/* 	for l in f_lines */
   3404 				/* 		if search lower in l lower */
   3405 				/* 			grep_lines append l */
   3406 				/* else */
   3407 				char *tid   = ssGet(tido);
   3408 				createTaskPath(dPath, "description.txt");
   3409 				createAllocateSmallArray(grep_lines);
   3410 				char c[16384];
   3411 				sprintf(c, "grep -i \"%s\" %s", search, dPath);
   3412 				execO(c, grep_lines, NULL);
   3413 				// add tid and filename to results
   3414 				sprintf(dPath, "/%s/description.txt:", tid);
   3415 				forEachSmallArray(grep_lines, H) {
   3416 					castS(h, H);
   3417 					createAllocateSmallArray(a);
   3418 					pushG(a, dPath);
   3419 					pushNFreeG(a, h);
   3420 					pushG(a, tid);
   3421 					pushNFreeG(grep_r, a);
   3422 				}
   3423 				terminateG(grep_lines);
   3424 			}
   3425 		}
   3426 		finishG(tido);
   3427 	}
   3428 
   3429 	// r lists all hits
   3430 	createAllocateSmallArray(r);
   3431 	forEachSmallArray(grep_r, L) {
   3432 		cast(smallArrayt *, l_l, L);
   3433 		// replace filename with tid and title (first line in file)
   3434 		// read first line in file
   3435 		char *hit_filename = getG(l_l, rtChar, 0);
   3436 		char task_path[8192];
   3437 		sprintf(task_path, "%s/%s", data_location_tasks, hit_filename);
   3438 		FILE *f = fopen(task_path, "r");
   3439 		char *title = readLineG(rtChar, f);
   3440 		trimG(&title);
   3441 		fclose(f);
   3442 		// set tid
   3443 		char *tid          = getG(l_l, rtChar, 2);
   3444 		char *group = "     ";
   3445 		if (is_this_task_a_group(tid)) {
   3446 			group = "GROUP";
   3447 		}
   3448 		if (is_linked(tid)) {
   3449 			group = " LINK";
   3450 		}
   3451 		char l[16384];
   3452 		sprintf(l, "%s %s - %s:%s", tid,group,title, getG(l_l, rtChar, 1));
   3453 		free(title);
   3454 		pushG(r, l);
   3455 		finishG(l_l);
   3456 	}
   3457 	terminateG(grep_r);
   3458 
   3459 	return r;
   3460 }
   3461 
   3462 /** search string in group
   3463  * @param[in] group task id
   3464  * @param[in] search query string
   3465  * @ingroup EDI_CORE
   3466  */
   3467 smallArrayt *search_string_in_group(const char *group, const char *search) {
   3468 
   3469 	smallArrayt *tasks  = readDirG(rtSmallArrayt, generate_group_path(group));
   3470 
   3471 	// r lists all hits
   3472 	createAllocateSmallArray(r);
   3473 	forEachSmallArray(tasks, O) {
   3474 		castS(orderidtid, O);
   3475 		// tid for current task in group
   3476 		char *tid   = ssGet(orderidtid)+ORDER_ID_LENGTH;
   3477 		// search in visible tasks only (filter enable status)
   3478 		const char *task_status = get_status(tid);
   3479 		createAllocateSmallArray(grep_r);
   3480 		if (getG(status_filters_d, rtI32, task_status) == ENABLE) {
   3481 			/* if platform.system() = 'Windows' */
   3482 			/* 	// in windows, grep in python */
   3483 			/* 	f          = open generate_task_path(tid)+os.sep+'description.txt' */
   3484 			/* 	f_lines    = f readlines */
   3485 			/* 	f close */
   3486 			/* 	grep_lines = [] */
   3487 			/* 	for l in f_lines */
   3488 			/* 		if search lower in l lower */
   3489 			/* 			grep_lines append l */
   3490 			/* else */
   3491 			createTaskPath(dPath, "description.txt");
   3492 			createAllocateSmallArray(grep_lines);
   3493 			char c[16384];
   3494 			sprintf(c, "grep -i \"%s\" %s", search, dPath);
   3495 			execO(c, grep_lines, NULL);
   3496 			// add tid and filename to results
   3497 			sprintf(dPath, "/%s/description.txt", tid);
   3498 			forEachSmallArray(grep_lines, H) {
   3499 				castS(h, H);
   3500 				createAllocateSmallArray(a);
   3501 				pushG(a, dPath);
   3502 				pushG(a, ssGet(h));
   3503 				pushG(a, tid);
   3504 				pushNFreeG(grep_r, a);
   3505 				finishG(h);
   3506 			}
   3507 			terminateG(grep_lines);
   3508 		}
   3509 
   3510 		forEachSmallArray(grep_r, L) {
   3511 			cast(smallArrayt *, l_l, L);
   3512 			// replace filename with tid and title (first line in file)
   3513 			// read first line in file
   3514 			char *hit_filename = getG(l_l, rtChar, 0);
   3515 			char task_path[8192];
   3516 			sprintf(task_path, "%s/%s", data_location_tasks, hit_filename);
   3517 			FILE *f = fopen(task_path, "r");
   3518 			if (!f) {
   3519 				pFuncError
   3520 				shEPrintfS("The path was: \"%s\"\n", task_path);
   3521 			}
   3522 			char *title = readLineG(rtChar, f);
   3523 			trimG(&title);
   3524 			fclose(f);
   3525 			// set tid
   3526 			tid         = getG(l_l, rtChar, 2);
   3527 			char *group = "     ";
   3528 			if (is_this_task_a_group(tid)) {
   3529 				group = "GROUP";
   3530 			}
   3531 			if (is_linked(tid)) {
   3532 				group = " LINK";
   3533 			}
   3534 			char l[16384];
   3535 			sprintf(l, "%s %s - %s:%s", tid,group,title, getG(l_l, rtChar, 1));
   3536 			free(title);
   3537 			pushG(r, l);
   3538 			finishG(l_l);
   3539 		}
   3540 		terminateG(grep_r);
   3541 		finishG(orderidtid);
   3542 	}
   3543 	terminateG(tasks);
   3544 	return r;
   3545 }
   3546 
   3547 /** search string in tree
   3548  * @param[in] group task id
   3549  * @param[in] search query string
   3550  * @ingroup EDI_CORE
   3551  */
   3552 smallArrayt *search_string_in_tree(const char *group, const char *search) {
   3553 	// walk_group is the list of groups to visit. FIFO
   3554 	createAllocateSmallArray(r);
   3555 	createAllocateSmallArray(walk_group);
   3556 	pushG(walk_group, group);
   3557 	// the while loop goes through all the group that are found
   3558 	while (lenG(walk_group)) {
   3559 		// list items in first group
   3560 		smallArrayt *tasks = readDirG(rtSmallArrayt ,generate_group_path(getG(walk_group, rtChar, 0)));
   3561 		appendNSmashG(r, search_string_in_group(getG(walk_group, rtChar,0), search));
   3562 
   3563 		// add group found in first group
   3564 		forEachSmallArray(tasks, T) {
   3565 			castS(t, T);
   3566 			// Check tasks that are not title task in a group
   3567 			if (not eqG(ssGet(t)+ORDER_ID_LENGTH, getG(walk_group, rtChar, 0)) and is_this_task_a_group(ssGet(t) + ORDER_ID_LENGTH)) {
   3568 				pushG(walk_group, ssGet(t)+ORDER_ID_LENGTH);
   3569 			}
   3570 			finishG(t);
   3571 		}
   3572 		terminateG(tasks);
   3573 
   3574 		// remove first group to list items in next group
   3575 		delG(walk_group, 0, 1);
   3576 	}
   3577 	terminateG(walk_group);
   3578 	return r;
   3579 }
   3580 
   3581 /* ## build tree recursively */
   3582 /* # @param[in] group group path in tree starting with os.sep */
   3583 /* # @return group_tree list of groups in tree starting from group */
   3584 /* # @ingroup EDI_CORE */
   3585 /* def build_group_tree group */
   3586 /* 	# list groups */
   3587 /* 	# call build_group_tree recursively */
   3588 /*  */
   3589 /* 	# list groups */
   3590 /* 	group_path    = generate_group_path(group.split(os.sep)[-1]) */
   3591 /*  */
   3592 /* 	groups        = sorted(os.listdir(group_path)) */
   3593 /*  */
   3594 /* 	if group != 'root' */
   3595 /* 		# first task is group title */
   3596 /* 		del groups[0] */
   3597 /* 	else */
   3598 /* 		# empty string for root */
   3599 /* 		group = '' */
   3600 /*  */
   3601 /* 	only_groups   = [] */
   3602 /* 	for g in groups */
   3603 /* 		if is_this_task_a_group(g[ORDER_ID_LENGTH:]) */
   3604 /* 			# complete tree path in only_groups */
   3605 /* 			only_groups append '%s%s%s' % (group, os.sep, g[ORDER_ID_LENGTH:]) */
   3606 /*  */
   3607 /* 	# call build_group_tree recursively */
   3608 /* 	group_tree = [] */
   3609 /* 	if only_groups */
   3610 /* 		for g in only_groups */
   3611 /* 			group_tree append g */
   3612 /* 			group_tree += build_group_tree(g) */
   3613 /* 	return group_tree */
   3614 /*  */
   3615 /* ## build tree in order */
   3616 /* # @param[in] group group path in tree or root */
   3617 /* # @return group_tree list of groups in tree starting from group */
   3618 /* # @ingroup EDI_CORE */
   3619 /* def build_tree group */
   3620 /* 	tidtree = build_group_tree(group) */
   3621 /* 	return tidtree */
   3622 /*  */
   3623 /* ## show tree */
   3624 /* #  @ingroup EDI_CORE */
   3625 /* # print all trees: group tids and titles */
   3626 /* def show_tree */
   3627 /* 	r       = [] */
   3628 /*  */
   3629 /* 	tidtree = build_tree('root') */
   3630 /*  */
   3631 /* 	# remove '/' from path */
   3632 /* 	tidtree = [i[1:] for i in tidtree] */
   3633 /*  */
   3634 /* 	# print titles path\n group tid path\n\n */
   3635 /* 	for l in tidtree */
   3636 /* 		# find title for group tids */
   3637 /* 		group_titles_in_path = [] */
   3638 /* 		for g in l strip.split(os.sep) */
   3639 /* 			group_titles_in_path append get_task_title(g) */
   3640 /* 		if user_interface = 'web' */
   3641 /* 			# convert / to - to be able to create links correctly */
   3642 /* 			group_titles_in_path = [i.replace(os.sep, '-') for i in group_titles_in_path] */
   3643 /* 		# create string title/title... */
   3644 /* 		group_titles_in_path_s = '/'.join(group_titles_in_path) */
   3645 /* 		r append '%s\n'%group_titles_in_path_s */
   3646 /* 		r append '%s\n'%l */
   3647 /*  */
   3648 /* 	return r */
   3649 /*  */
   3650 /* ## group statistics */
   3651 /* #  @ingroup EDI_CORE */
   3652 /* # print statistics for a group recursively */
   3653 /* def group_statistics group */
   3654 /* 	global stats */
   3655 /* 	global stats_total */
   3656 /* 	global stats_creation_dates */
   3657 /* 	global stats_overtime */
   3658 /*  */
   3659 /* 	# compute total number of tasks in group */
   3660 /* 	path = generate_group_path(group) */
   3661 /*  */
   3662 /* 	tasks         = [] */
   3663 /* 	# remove group title */
   3664 /* 	for i in os.listdir(path) */
   3665 /* 		if not baseconvert(0) in i[:ORDER_ID_LENGTH] */
   3666 /* 			tasks append i[ORDER_ID_LENGTH:] */
   3667 /*  */
   3668 /* 	stats_total   += len(tasks) */
   3669 /*  */
   3670 /* 	# compute number of tasks in each state, groups and links */
   3671 /* 	for tid in tasks */
   3672 /* 		task_status        = get_status(tid) */
   3673 /* 		stats[task_status] += 1 */
   3674 /* 		if is_this_task_a_group(tid) */
   3675 /* 			stats['   Group'] += 1 */
   3676 /* 			group_statistics(tid) */
   3677 /* 		if is_linked(tid) */
   3678 /* 			stats['  Linked'] += 1 */
   3679 /*  */
   3680 /* 		#stats_creation_dates */
   3681 /* 		# Figure out creation time and modification time for description */
   3682 /* 		task_ctime         = get_creation_date(tid)[1] */
   3683 /*  */
   3684 /* 		task_path         = generate_task_path(tid) */
   3685 /* 		(mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime) = os.stat(task_path+os.sep+'description.txt') */
   3686 /* 		# 'if' below to be compatible with first database format */
   3687 /* 		if task_ctime = 0 */
   3688 /* 			# ctime not available */
   3689 /* 			task_ctime = mtime */
   3690 /* 		stats_creation_dates append task_ctime */
   3691 /*  */
   3692 /* 		(mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime) = os.stat(task_path+os.sep+'status') */
   3693 /*  */
   3694 /* 		cdate = time.strftime("%Y-%m-%d", time.localtime(task_ctime)) */
   3695 /* 		sdate = time.strftime("%Y-%m-%d", time.localtime(mtime)) */
   3696 /* 		if not stats_overtime.has_key(cdate) */
   3697 /* 			# initialize date dict */
   3698 /* 			stats_overtime[cdate] = dict(zip(STATS_OVERTIME_KEYS,[0 for i in range(len(STATS_OVERTIME_KEYS))])) */
   3699 /* 		stats_overtime[cdate]['Creation'] += 1 */
   3700 /* 		if not stats_overtime.has_key(sdate) */
   3701 /* 			# initialize date dict */
   3702 /* 			stats_overtime[sdate] = dict(zip(STATS_OVERTIME_KEYS,[0 for i in range(len(STATS_OVERTIME_KEYS))])) */
   3703 /* 		stats_overtime[sdate][task_status] += 1 */
   3704 /* 		#end */
   3705 /*  */
   3706 /* ## statistics */
   3707 /* #  @ingroup EDI_CORE */
   3708 /* # print statistics for a group or a database */
   3709 /* # compute speed */
   3710 /* def statistics group */
   3711 /* 	global stats */
   3712 /* 	global stats_total */
   3713 /* 	global stats_creation_dates */
   3714 /* 	global stats_overtime */
   3715 /* 	r = [] */
   3716 /*  */
   3717 /* 	# initialize stats dictionary */
   3718 /* 	stat_keys     = [TASK_STATUS[i] for i in range(len(TASK_STATUS))] */
   3719 /* 	stat_keys append '   Group' */
   3720 /* 	stat_keys append '  Linked' */
   3721 /* 	state_amounts = [0 for i in range(len(stat_keys))] */
   3722 /* 	stats         = dict(zip(stat_keys,state_amounts)) */
   3723 /*  */
   3724 /* 	if group = 'for database' */
   3725 /* 		# compute total number of tasks, excluding root */
   3726 /* 		path          = data_location_tasks */
   3727 /* 		tasks         = [] */
   3728 /* 		for i in os.listdir(path) */
   3729 /* 			if i != 'root' */
   3730 /* 				tasks append i */
   3731 /*  */
   3732 /* 		# compute number of tasks in each state, groups and links */
   3733 /* 		for tid in tasks */
   3734 /* 			task_status        = get_status(tid) */
   3735 /* 			stats[task_status] += 1 */
   3736 /* 			if is_this_task_a_group(tid) */
   3737 /* 				stats['   Group'] += 1 */
   3738 /* 			if is_linked(tid) */
   3739 /* 				stats['  Linked'] += 1 */
   3740 /*  */
   3741 /* #:define stats_creation_dates */
   3742 /* 			# Figure out creation time and modification time for description */
   3743 /* 			task_ctime         = get_creation_date(tid)[1] */
   3744 /*  */
   3745 /* 			task_path         = generate_task_path(tid) */
   3746 /* 			(mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime) = os.stat(task_path+os.sep+'description.txt') */
   3747 /* 			if task_ctime = 0 */
   3748 /* 				# ctime not available */
   3749 /* 				task_ctime = mtime */
   3750 /* 			stats_creation_dates append task_ctime */
   3751 /*  */
   3752 /* 			(mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime) = os.stat(task_path+os.sep+'status') */
   3753 /*  */
   3754 /* 			cdate = time.strftime("%Y-%m-%d", time.localtime(task_ctime)) */
   3755 /* 			sdate = time.strftime("%Y-%m-%d", time.localtime(mtime)) */
   3756 /* 			if not stats_overtime.has_key(cdate) */
   3757 /* 				# initialize date dict */
   3758 /* 				stats_overtime[cdate] = dict(zip(STATS_OVERTIME_KEYS,[0 for i in range(len(STATS_OVERTIME_KEYS))])) */
   3759 /* 			stats_overtime[cdate]['Creation'] += 1 */
   3760 /* 			if not stats_overtime.has_key(sdate) */
   3761 /* 				# initialize date dict */
   3762 /* 				stats_overtime[sdate] = dict(zip(STATS_OVERTIME_KEYS,[0 for i in range(len(STATS_OVERTIME_KEYS))])) */
   3763 /* 			stats_overtime[sdate][task_status] += 1 */
   3764 /* #:end */
   3765 /*  */
   3766 /* 		stats_total   = len(tasks) */
   3767 /* 	else */
   3768 /* 		# compute total number of tasks in group */
   3769 /* 		path = generate_group_path(group) */
   3770 /*  */
   3771 /* 		tasks         = [] */
   3772 /* 		for i in os.listdir(path) */
   3773 /* 			if group = 'root' */
   3774 /* 				tasks append i[ORDER_ID_LENGTH:] */
   3775 /* 			else */
   3776 /* 				# the group task is not counted in the statistics */
   3777 /* 				if not baseconvert(0) in i[:ORDER_ID_LENGTH] */
   3778 /* 					tasks append i[ORDER_ID_LENGTH:] */
   3779 /*  */
   3780 /* 		stats_total   = len(tasks) */
   3781 /*  */
   3782 /* 		# compute number of tasks in each state, groups and links, recursively */
   3783 /* 		for tid in tasks */
   3784 /* 			task_status        = get_status(tid) */
   3785 /* 			stats[task_status] += 1 */
   3786 /* 			if is_this_task_a_group(tid) */
   3787 /* 				stats['   Group'] += 1 */
   3788 /* 				group_statistics(tid) */
   3789 /* 			if is_linked(tid) */
   3790 /* 				stats['  Linked'] += 1 */
   3791 /*  */
   3792 /* #:stats_creation_dates */
   3793 /*  */
   3794 /* 	if not stats_total */
   3795 /* 		r append '0 task in statistics.' */
   3796 /* 		return r */
   3797 /*  */
   3798 /* 	# csv format, not used for now */
   3799 /* 	#col_names = ['Tasks'] + sorted(stats.keys()) */
   3800 /* 	#col_names = [i strip for i in col_names] */
   3801 /* 	#print ','.join(col_names) */
   3802 /* 	#csv = '%d'%len(tasks) */
   3803 /* 	#for k in sorted(stats.keys()) */
   3804 /* 	#	csv += ',%d'%stats[k] */
   3805 /* 	#r append csv */
   3806 /* 	#r append '' */
   3807 /*  */
   3808 /* 	r append 'Number of items: %d\n'%stats_total */
   3809 /* 	for k in sorted(stats.keys()) */
   3810 /* 		r append '%s - %d'%(k, stats[k]) */
   3811 /* 	r append '' */
   3812 /*  */
   3813 /* 	# speed */
   3814 /* 	start_time        = sorted(stats_creation_dates)[0] */
   3815 /* 	now               = time.time() */
   3816 /* 	done_and_inactive = stats[TASK_STATUS[TASK_STATUS_DONE]] + stats[TASK_STATUS[TASK_STATUS_INACTIVE]] */
   3817 /* 	if not done_and_inactive */
   3818 /* 		r append 'Nothing is done or inactive' */
   3819 /* 	else */
   3820 /* 		# subtract tasks in state void, because tasks in void state are informative or groups */
   3821 /* 		number_of_tasks   = stats_total - stats[TASK_STATUS[TASK_STATUS_VOID]] */
   3822 /* 		remaining_tasks   = number_of_tasks - done_and_inactive */
   3823 /* 		remaining_time    = (now - start_time) / float(done_and_inactive) * remaining_tasks */
   3824 /* 		r append 'Number of tasks (excluding voids): %d'%number_of_tasks */
   3825 /* 		r append 'Remaining tasks:                   %d'%remaining_tasks */
   3826 /* 		r append 'Remaining days:                    %.3f'%(remaining_time/86400) */
   3827 /* 		r append 'Start date:                        %s'%time.strftime("%Y-%m-%d", time.localtime(start_time)) */
   3828 /* 		r append "Today's date:                      %s"%time.strftime("%Y-%m-%d", time.localtime(now)) */
   3829 /* 		r append 'Finish date:                       %s'%time.strftime("%Y-%m-%d", time.localtime(now + remaining_time)) */
   3830 /*  */
   3831 /* 	# create csv stats from stats_overtime */
   3832 /* 	f = open data_location+os.sep+'stats.csv' w */
   3833 /* 	f write 'date,%s\n'%','.join([i strip for i in STATS_OVERTIME_KEYS]) */
   3834 /*  */
   3835 /* 	for d in sorted(stats_overtime keys) */
   3836 /* 		f write '%s,%s\n'%(d, ','.join([str(stats_overtime[d][i]) for i in STATS_OVERTIME_KEYS])) */
   3837 /* 	f close */
   3838 /*  */
   3839 /* 	# create creation, done and inactive stats */
   3840 /* 	f = open data_location+os.sep+'stats_creation_done_inactive.csv' w */
   3841 /* 	f write 'date,Creation,Done/Inactive\n' */
   3842 /*  */
   3843 /* 	for d in sorted(stats_overtime keys) */
   3844 /* 		f write '%s,%d,%d\n'%(d, stats_overtime[d]['Creation'], stats_overtime[d][TASK_STATUS[TASK_STATUS_DONE]] + stats_overtime[d][TASK_STATUS[TASK_STATUS_INACTIVE]]) */
   3845 /* 	f close */
   3846 /*  */
   3847 /* 	r append '' */
   3848 /* 	return r */
   3849 /*  */
   3850 /* ## create secret */
   3851 /* #  @ingroup EDI_CORE */
   3852 /* # encrypts description.txt in tid */
   3853 /* # the title remains clear */
   3854 /* def create_secret tid */
   3855 /*  */
   3856 /* 	# encrypt description.txt to description.txt.gpg */
   3857 /* 	tdir = generate_task_path(tid) +os.sep */
   3858 /* 	path = tdir+'description.txt' */
   3859 /* #:define encrypt */
   3860 /* 	os.system 'gpg -c --cipher-algo AES256 %s' % path */
   3861 /* 	# delete clear text */
   3862 /* 	os.rename(path, tdir+'tmp') */
   3863 /* 	# create clear title */
   3864 /* 	os.system 'head -n 1 %s > %s' % (tdir+'tmp', path) */
   3865 /* 	os.remove tdir+'tmp' */
   3866 /* #:end */
   3867 /*  */
   3868 /* ## edit secret */
   3869 /* #  @ingroup EDI_CORE */
   3870 /* # decrypts description.txt.gpg to description.txt */
   3871 /* # starts text editor */
   3872 /* # encrypts description.txt in tid */
   3873 /* # the title remains clear */
   3874 /* def edit_secret tid */
   3875 /*  */
   3876 /* 	# delete clear description.txt (holding the title) */
   3877 /* 	tdir = generate_task_path(tid) +os.sep */
   3878 /* 	path = tdir+'description.txt' */
   3879 /* 	os.remove path */
   3880 /* 	os.system 'gpg %s' % (path+'.gpg') */
   3881 /* 	os.remove path+'.gpg' */
   3882 /* 	# start text editor */
   3883 /* 	edit_task(tid) */
   3884 /* #:encrypt */
   3885 /*  */
   3886 /* ## for your eyes only */
   3887 /* #  @ingroup EDI_CORE */
   3888 /* # print secret and hide passwords */
   3889 /* def fyeo tid */
   3890 /*  */
   3891 /* 	# decrypt description.txt.gpg to stdout */
   3892 /* 	f = os.popen 'gpg -d %s' % generate_task_path(tid) +os.sep+'description.txt.gpg' */
   3893 /* 	t = f readlines */
   3894 /* 	f close */
   3895 /* 	for l in t */
   3896 /* 		# hide text after the word password on lines starting with password */
   3897 /* 		if l lower[:8] = 'password' */
   3898 /* 			L = l strip.split(' ') */
   3899 /* 			p = L[0] */
   3900 /* 			del L[0] */
   3901 /* 			s = ' '.join(L) */
   3902 /* 			print '%s \033[1;8m\033[1;41m%s\033[0;0m' %(p, s) */
   3903 /* 		else */
   3904 /* 			# print normal text */
   3905 /* 			print l rstrip */
   3906 /*  */
   3907 
   3908 /**
   3909  * list text for all tasks in list
   3910  * @param
   3911  *   tid group to show
   3912  * @return
   3913  *   array containing all task descriptions in group
   3914  */
   3915 smallArrayt *listTasksInList(smallArrayt *ls) {
   3916 	createAllocateSmallArray(r);
   3917 	forEachSmallArray(ls, T) {
   3918 		cast(smallDictt*, d, T);
   3919 		if (not eqG(getG(d, rtChar, "tid"), "")) {
   3920 			smallArrayt *desc = display_task(getG(d, rtChar, "tid"), passThroughTitle);
   3921 			pushG(r, "---");
   3922 			appendNSmashG(r, desc);
   3923 		}
   3924 		finishG(d);
   3925 	}
   3926 	return r;
   3927 }
   3928 
   3929 /**
   3930  * list text for all tasks in group tid
   3931  * @param
   3932  *   tid group to show
   3933  * @return
   3934  *   array containing all task descriptions in group
   3935  */
   3936 smallArrayt *listTasksInGroup(const char *tid) {
   3937 	createAllocateSmallArray(r);
   3938 	if (is_this_task_a_group(tid)) {
   3939 		// list_group...
   3940 		smallArrayt *ls = list_group(tid);
   3941 		forEachSmallArray(ls, T) {
   3942 			cast(smallDictt*, d, T);
   3943 			if (not eqG(getG(d, rtChar, "tid"), "")) {
   3944 				smallArrayt *desc = display_task(getG(d, rtChar, "tid"), passThroughTitle);
   3945 				pushG(r, "---");
   3946 				appendNSmashG(r, desc);
   3947 			}
   3948 			finishG(d);
   3949 		}
   3950 		terminateG(ls);
   3951 	}
   3952 	return r;
   3953 }
   3954 
   3955 /**
   3956  * list text in bookmarks
   3957  * @return
   3958  *   array containing all task descriptions in the bookmarks
   3959  */
   3960 smallArrayt *listTasksInBookmarks(void) {
   3961 	createAllocateSmallArray(r);
   3962 	forEachSmallArray(bookmarks, T) {
   3963 		castS(s, T);
   3964 		smallArrayt *desc = display_task(ssGet(s), passThroughTitle);
   3965 		pushG(r, "---");
   3966 		appendNSmashG(r, desc);
   3967 		finishG(s);
   3968 	}
   3969 	return r;
   3970 }
   3971 
   3972 /** clear edi_core buffers
   3973  * @ingroup EDI_DESKTOP
   3974  */
   3975 void edi_core_clear_previous_run(void) {
   3976 	emptyG(group_directory_file_list);
   3977 	if (agenda) {
   3978 		emptyG(agenda);
   3979 	}
   3980 }
   3981 
   3982 /** search tid in all selected databases
   3983  * @return r empty when tid is not found, 'tid exists' when found
   3984  * @param[in] tid task or group id
   3985  * @ingroup EDI_DESKTOP
   3986  * The setup of the database for tid is kept
   3987  */
   3988 bool tid_exists(const char *tid) {
   3989 	bool r = false;
   3990 	// clear edi_core variable because the database setup can change
   3991 	edi_core_clear_previous_run();
   3992 	forEachSmallArray(selected_path, P) {
   3993 		castS(path, P);
   3994 		freeManyS(data_location, data_location_tasks, data_location_groups, data_location_tree);
   3995 		data_location        = dupG(ssGet(path));
   3996 
   3997 		data_location_tasks  = appendG(data_location, "/tasks");
   3998 		data_location_groups = appendG(data_location, "/groups");
   3999 		data_location_tree   = appendG(data_location, "/tree");
   4000 
   4001 		if (not fileExists(data_location)) {
   4002 			//strcpy(result_select_database, data_location);
   4003 			//strcat(result_select_database, " is unreachable");
   4004 			finishG(P);
   4005 			break;
   4006 		}
   4007 
   4008 		if (fileExists(generate_task_path(tid))) {
   4009 			r = true;
   4010 			finishG(P);
   4011 			break;
   4012 		}
   4013 		finishG(P);
   4014 	}
   4015 	return r;
   4016 }
   4017 
   4018 /** search tid in all selected databases
   4019  * @param[in] tid task or group id
   4020  * @ingroup EDI_DESKTOP
   4021  * The setup of the database for tid is kept.
   4022  * Same as tid_exists without return value.
   4023  */
   4024 bool setup_data_location_for_tid(const char *tid) {
   4025 	return tid_exists(tid);
   4026 }
   4027 
   4028 /** save current database path in saved_data_location
   4029  * @ingroup EDI_DESKTOP
   4030  */
   4031 void save_edi_core_data_location(void) {
   4032 	//debug print 'save'
   4033 	free(saved_data_location);
   4034 	saved_data_location = dupG(data_location);
   4035 }
   4036 
   4037 /** restore saved_data_location
   4038  * @ingroup EDI_DESKTOP
   4039  */
   4040 void restore_edi_core_data_location(void) {
   4041 	//debug print 'restore'
   4042 
   4043 	freeManyS(data_location, data_location_tasks, data_location_groups, data_location_tree);
   4044 	data_location        = saved_data_location;
   4045 	saved_data_location  = NULL;
   4046 
   4047 	data_location_tasks  = appendG(data_location, "/tasks");
   4048 	data_location_groups = appendG(data_location, "/groups");
   4049 	data_location_tree   = appendG(data_location, "/tree");
   4050 
   4051 	// clear edi_core variable because the database setup can change
   4052 	edi_core_clear_previous_run();
   4053 }
   4054 
   4055 char return_getDatabaseNameFromPath[8192];
   4056 
   4057 const char *getDatabaseNameFromPath(const char *database_path) {
   4058 	return_getDatabaseNameFromPath[0] = 0;
   4059 	forEachSmallDict(selected_d, k, V) {
   4060 		castS(v, V);
   4061 		if (eqG(v, database_path)) {
   4062 			strcpy(return_getDatabaseNameFromPath, k);
   4063 			finishG(v);
   4064 			break;
   4065 		}
   4066 		finishG(v);
   4067 	}
   4068 	listFreeS(libsheepyInternalKeys);
   4069 	return return_getDatabaseNameFromPath;
   4070 }
   4071 
   4072 // test core functions
   4073 void test(void) {
   4074 	// test
   4075 	group_directory_file_list = walkDirG(rtSmallArrayt, data_location_groups);
   4076 	logG(group_directory_file_list);
   4077 	return;
   4078 	smallArrayt *ls = list_group("root");
   4079 
   4080 	forEachSmallArray(ls, L) {
   4081 		cast(smallDictt*, d, L)
   4082 		putsG(d);
   4083 		finishG(d);
   4084 	}
   4085 	forEachSmallArray(ls, L) {
   4086 		cast(smallDictt*, d, L)
   4087 		putsG(getG(d, rtChar, "title"));
   4088 		finishG(d);
   4089 	}
   4090 	terminateG(ls);
   4091 
   4092 	smallArrayt *desc = display_task("RUkjgkUSsDYREKqM", generate_task_string_with_tid);
   4093 	logG(desc);
   4094 	terminateG(desc);
   4095 
   4096 	/* t1 = add_task('root', 'task1.txt') */
   4097 	/* add_task('root', 'task2.txt') */
   4098 	/* g3 = add_task('root', 'task3.txt') */
   4099         /*  */
   4100 	/* create_group(g3) */
   4101 	/* t4 = add_task(g3, 'task4.txt') */
   4102         /*  */
   4103 	/* list_group('root') */
   4104 	/* list_group(g3) */
   4105         /*  */
   4106 	/* //display_task(t1) */
   4107 	/* //display_task(t4) */
   4108         /*  */
   4109 	/* // Delete task in a group of 2 tasks */
   4110 	/* print '// Delete task in a group of 2 tasks' */
   4111 	/* delete_task(t4) */
   4112         /*  */
   4113 	/* list_group('root') */
   4114         /*  */
   4115 	/* create_group(g3) */
   4116 	/* t4 = add_task(g3, 'task4.txt') */
   4117         /*  */
   4118 	/* list_group('root') */
   4119         /*  */
   4120 	/* // Delete group first task */
   4121 	/* print '// Delete group first task' */
   4122 	/* delete_task(g3) */
   4123         /*  */
   4124 	/* list_group('root') */
   4125         /*  */
   4126 	/* // Delete group task of a group with more than 2 tasks */
   4127 	/* print '// Delete group task of a group with more than 2 tasks' */
   4128 	/* delete_task(t4) */
   4129         /*  */
   4130 	/* g3 = add_task('root', 'task3.txt') */
   4131 	/* create_group(g3) */
   4132 	/* t4 = add_task(g3, 'task4.txt') */
   4133 	/* t2 = add_task(g3, 'task2.txt') */
   4134         /*  */
   4135 	/* list_group('root') */
   4136 	/* list_group(g3) */
   4137 	/* new_group_id = delete_task(g3) */
   4138 	/* print 'New group id %s'%new_group_id */
   4139         /*  */
   4140 	/* list_group('root') */
   4141 	/* list_group(new_group_id) */
   4142         /*  */
   4143 	/* // Delete group */
   4144 	/* print '// Delete group' */
   4145 	/* delete_group(new_group_id) */
   4146         /*  */
   4147 	/* list_group('root') */
   4148         /*  */
   4149 	/* print baseconvert(100) */
   4150 	/* print baseconvert_to_dec(baseconvert(100)) */
   4151 	/* print */
   4152         /*  */
   4153 	/* //edit_task(t1) */
   4154         /*  */
   4155 	/* // Change order */
   4156 	/* print '// Change order' */
   4157 	/* g3 = add_task('root', 'task3.txt') */
   4158 	/* change_task_order('root',1,0) */
   4159 	/* change_task_order('root',0,2) */
   4160         /*  */
   4161 	/* list_group('root') */
   4162         /*  */
   4163 	/* // Change status */
   4164 	/* print '// Change status' */
   4165 	/* create_group(g3) */
   4166 	/* t4 = add_task(g3, 'task4.txt') */
   4167 	/* t2 = add_task(g3, 'task2.txt') */
   4168         /*  */
   4169 	/* list_group('root') */
   4170         /*  */
   4171 	/* set_status(t1,TASK_STATUS_DONE) */
   4172 	/* set_status(t4,TASK_STATUS_DONE) */
   4173         /*  */
   4174 	/* list_group('root') */
   4175 	/* list_group(g3) */
   4176         /*  */
   4177 	/* // Reset status */
   4178         /*  */
   4179 	/* reset_group_status(g3) */
   4180         /*  */
   4181 	/* list_group('root') */
   4182 	/* list_group(g3) */
   4183         /*  */
   4184 	/* reset_group_status('root') */
   4185         /*  */
   4186 	/* list_group('root') */
   4187 	/* list_group(g3) */
   4188         /*  */
   4189 	/* // Create a group in group */
   4190 	/* print '// Create a group in group' */
   4191 	/* create_group(t4) */
   4192 	/* t2 = add_task(t4,'task2.txt') */
   4193         /*  */
   4194 	/* list_group('root') */
   4195 	/* list_group(g3) */
   4196 	/* list_group(t4) */
   4197         /*  */
   4198 	/* // Delete task in a group of 2 tasks not in root */
   4199 	/* print '// Delete task in a group of 2 tasks not in root' */
   4200 	/* delete_task(t2) */
   4201         /*  */
   4202 	/* list_group('root') */
   4203 	/* list_group(g3) */
   4204         /*  */
   4205 	/* // Delete group with groups in it */
   4206 	/* print '// Delete group with groups in it' */
   4207 	/* create_group(t4) */
   4208 	/* t2 = add_task(t4,'task2.txt') */
   4209         /*  */
   4210 	/* list_group('root') */
   4211 	/* list_group(g3) */
   4212 	/* list_group(t4) */
   4213         /*  */
   4214 	/* delete_group(g3) */
   4215         /*  */
   4216 	/* list_group('root') */
   4217         /*  */
   4218 	/* // Search string */
   4219 	/* print '// Search string' */
   4220         /*  */
   4221 	/* search_string('AND') */
   4222         /*  */
   4223 	/* // List tree */
   4224 	/* print '// List tree' */
   4225 	/* g3 = add_task('root', 'task3.txt') */
   4226 	/* create_group(g3) */
   4227 	/* t4 = add_task(g3, 'task4.txt') */
   4228 	/* t2 = add_task(g3, 'task2.txt') */
   4229 	/* create_group(t4) */
   4230 	/* t2 = add_task(t4,'task2.txt') */
   4231         /*  */
   4232 	/* list_tree('root') */
   4233         /*  */
   4234 	/* // Show group for a task */
   4235 	/* print '// Show group for a task' */
   4236         /*  */
   4237 	/* print 'Show group for %s - %s' %(t2,get_task_title(t2)) */
   4238 	/* show_group_for_task(t2) */
   4239 	/* print 'Show group for %s - %s' %(t4,get_task_title(t4)) */
   4240 	/* show_group_for_task(t4) */
   4241 	/* print 'Show group for %s - %s' %(t1,get_task_title(t1)) */
   4242 	/* show_group_for_task(t1) */
   4243 
   4244 	// create task
   4245 	//create_task(t4)
   4246 	//list_group(t4)
   4247 }
   4248 
   4249 /** start - always called at startup.
   4250  * @ingroup EDI_CORE
   4251  * @param[in] interface selects current user interface. cli loads the configuration from user home, web loads the configuration located in edi_web folder.
   4252  * creates default .easydoneit.ini<br>
   4253  * creates database folders<br>
   4254  * loads .easydoneit.ini
   4255  */
   4256 void start(char *interface) {
   4257 	// steps
   4258 	// initialize variables
   4259 	// create default config
   4260 	// load config from inipath
   4261 
   4262 	// initialize variables
   4263 	add_top_or_bottom = strdup("bottom");
   4264 	status_filters    = allocG(rtSmallArrayt);
   4265 	//static i16 no_color[4]            = {-1,-1,-1,255};
   4266 	//static i16 status_fgColors[6][4]  = {{0,0,0,255},{0,255,0,255},{255,128,0,255},{255,0,0,255},{192,192,192,255},{0,0,0,255}};
   4267 	no_color          = allocG(rtSmallArrayt);
   4268 	pushG(no_color, -1);
   4269 	pushG(no_color, -1);
   4270 	pushG(no_color, -1);
   4271 	pushG(no_color, 255);
   4272 	status_fgColors   = allocG(rtSmallArrayt);
   4273 	smallArrayt *c    = allocG(rtSmallArrayt);
   4274 	pushG(c, 0);
   4275 	pushG(c, 0);
   4276 	pushG(c, 0);
   4277 	pushG(c, 255);
   4278 	pushNFreeG(status_fgColors, dupG(c));
   4279 	setG(c, 0, 0);
   4280 	setG(c, 1, 255);
   4281 	setG(c, 2, 0);
   4282 	setG(c, 3, 255);
   4283 	pushNFreeG(status_fgColors, dupG(c));
   4284 	setG(c, 0, 255);
   4285 	setG(c, 1, 128);
   4286 	setG(c, 2, 0);
   4287 	setG(c, 3, 255);
   4288 	pushNFreeG(status_fgColors, dupG(c));
   4289 	setG(c, 0, 255);
   4290 	setG(c, 1, 0);
   4291 	setG(c, 2, 0);
   4292 	setG(c, 3, 255);
   4293 	pushNFreeG(status_fgColors, dupG(c));
   4294 	setG(c, 0, 192);
   4295 	setG(c, 1, 192);
   4296 	setG(c, 2, 192);
   4297 	setG(c, 3, 255);
   4298 	pushNFreeG(status_fgColors, dupG(c));
   4299 	setG(c, 0, 0);
   4300 	setG(c, 1, 0);
   4301 	setG(c, 2, 0);
   4302 	setG(c, 3, 255);
   4303 	pushNFreeG(status_fgColors, dupG(c));
   4304 	status_bgColors   = allocG(rtSmallArrayt);
   4305 	range(i, COUNT_ELEMENTS(TASK_STATUS)-1) {
   4306 		pushNFreeG(status_bgColors, dupG(no_color));
   4307 	}
   4308 	//logVarG(no_color);
   4309 	//logVarG(status_fgColors);
   4310 	//logVarG(status_bgColors);
   4311 	group_directory_file_list = allocG(rtSmallArrayt);
   4312 
   4313 	inipath           = "~/.easydoneit.ini";
   4314 	inipath           = expandHomeG(inipath);
   4315 
   4316 	user_interface = interface;
   4317 
   4318 	if (not eqG(user_interface, "tui")) {
   4319 		puts(user_interface);
   4320 		puts(BLD RED "Not supported." RST);
   4321 		XFAILURE
   4322 	}
   4323 	else {
   4324 		if (not fileExists(inipath)) {
   4325 			// create default config
   4326 			createAllocateSmallArray(newcfg);
   4327 			pushG(newcfg, "[data]");
   4328 			pushG(newcfg, "location=~/easydoneit_data");
   4329 			pushG(newcfg, "1=~/easydoneit_data");
   4330 			pushG(newcfg, "");
   4331 			pushG(newcfg, "[locations]");
   4332 			pushG(newcfg, "selected=1");
   4333 			pushG(newcfg, "default_add_in=1");
   4334 			pushG(newcfg, "");
   4335 			pushG(newcfg, "[filters]");
   4336 			// enable all status filters
   4337 			char buf[1024];
   4338 			forEachS(TASK_STATUS, nfilter) {
   4339 				// strip to remove spaces in status strings
   4340 				char *filter = lowerS(nfilter);
   4341 				trimG(&filter);
   4342 				sprintf(buf, "%s= %s", filter, STATUS_FILTER_STATES[0]);
   4343 				free(filter);
   4344 				pushG(newcfg, buf);
   4345 			}
   4346 			pushG(newcfg, "");
   4347 			pushG(newcfg, "[colors]");
   4348 			enumerateS(TASK_STATUS, nfilter, n) {
   4349 				char *filter = lowerS(nfilter);
   4350 				trimG(&filter);
   4351 				smallArrayt *c = getG(status_fgColors, rtSmallArrayt, n);
   4352 				sprintf(buf, "%s_fgColor=%d,%d,%d,%d", filter, getG(c, rtI32, 0),  getG(c, rtI32, 1), getG(c, rtI32, 2), getG(c, rtI32, 3));
   4353 				finishG(c);
   4354 				free(filter);
   4355 				pushG(newcfg, buf);
   4356 			}
   4357 			{enumerateS(TASK_STATUS, nfilter, n) {
   4358 				char *filter = lowerS(nfilter);
   4359 				trimG(&filter);
   4360 				smallArrayt *c = getG(status_bgColors, rtSmallArrayt, n);
   4361 				sprintf(buf, "%s_bgColor=%d,%d,%d,%d", filter, getG(c, rtI32, 0),  getG(c, rtI32, 1), getG(c, rtI32, 2), getG(c, rtI32, 3));
   4362 				finishG(c);
   4363 				free(filter);
   4364 				pushG(newcfg, buf);
   4365 			}}
   4366 
   4367 			pushG(newcfg, "\n[settings]");
   4368 			pushG(newcfg, "editor=vi");
   4369 
   4370 			writeFileG(newcfg, inipath);
   4371 			terminateG(newcfg);
   4372 		}
   4373 	}
   4374 
   4375 	// load config from inipath
   4376 	ini = parseIni(inipath);
   4377 
   4378 	smallDictt *data      = getG(ini, rtSmallDictt, "data");
   4379 	smallDictt *locations = getG(ini, rtSmallDictt, "locations");
   4380 	smallDictt *filters   = getG(ini, rtSmallDictt, "filters");
   4381 	smallDictt *colors    = getG(ini, rtSmallDictt, "colors");
   4382 	smallDictt *settings  = getG(ini, rtSmallDictt, "settings");
   4383 	smallDictt *desktop   = getG(ini, rtSmallDictt, "desktop");
   4384 
   4385 	//logVarG(filters);
   4386 
   4387 	// convert ~ to home path
   4388 	data_location = expandHomeG(getG(data, rtChar,"location"));
   4389 
   4390 	// load available databases
   4391 	char **data_keys          = keysG(data);
   4392 	smallArrayt *data_section = valuesG(data);
   4393 	// remove location which is a path to a database
   4394 	// clear databases for reentrant testing
   4395 	databases = allocG(rtSmallArrayt);
   4396 	enumerateSmallArray(data_section, D, i) {
   4397 		if (not eqG(data_keys[i], "location")) {
   4398 			pushNFreeG(databases, dupG(D));
   4399 		}
   4400 		free(D);
   4401 	}
   4402 	//logVarG(databases);
   4403 
   4404 	// load add_top_or_bottom
   4405 	char **location_keys      = keysG(locations);
   4406 	if (hasG(location_keys, "add_top_or_bottom")) {
   4407 		add_top_or_bottom = getNDupG(locations, rtChar,"add_top_or_bottom");
   4408 	}
   4409 
   4410 	// load selected database paths
   4411 	smallStringt *sselected = getNDupG(locations, rtSmallStringt, "selected");
   4412 	selected            = splitG(sselected, ",");
   4413 	terminateG(sselected);
   4414 	default_add_in      = getNDupG(locations,rtChar, "default_add_in");
   4415 	selected_path       = allocG(rtSmallArrayt);
   4416 	forEachSmallArray(selected, D) {
   4417 		castS(d, D);
   4418 		pushNFreeG(selected_path, expandHomeG(getG(data, rtChar, ssGet(d))));
   4419 		free(D);
   4420 	}
   4421 
   4422 	selected_d = allocG(rtSmallDictt);
   4423 	zipG(selected_d, selected, selected_path);
   4424 	//logVarG(selected_path);
   4425 	//logVarG(selected_d);
   4426 
   4427 	// load autolink groups
   4428 	if (hasG(location_keys, "autolink")) {
   4429 		smallStringt *autolink_cfg = getG(locations, rtSmallStringt,"autolink");
   4430 		autolink                   = allocG(rtSmallArrayt);
   4431 		rangeStep(i, lenG(autolink_cfg), ID_LENGTH+1) {
   4432 			smallStringt *s = copyRngG(autolink_cfg, i, i+ID_LENGTH);
   4433 			pushNFreeG(autolink, s);
   4434 		}
   4435 		finishG(autolink_cfg);
   4436 	}
   4437 	//logVarG(autolink);
   4438 
   4439 	// load list groups
   4440 	if (hasG(location_keys, "list")) {
   4441 		smallStringt *list_cfg = getG(locations, rtSmallStringt, "list");
   4442 		list_of_groups  = splitG(list_cfg, ",");
   4443 	}
   4444 
   4445 	// load status filters and status colors
   4446 	char **config_filter_names = keysG(filters);
   4447 	char **config_color_names  = keysG(colors);
   4448 	enumerateS(TASK_STATUS, nfilter, n) {
   4449 		// strip to remove spaces in status strings
   4450 		// check that task_status filter is in config file, to avoid problems between config file versions
   4451 		char *filter = lowerS(nfilter);
   4452 		trimG(&filter);
   4453 		pushG(&TASK_STATUS_TRIM, filter);
   4454 		if (hasG(config_filter_names, filter)) {
   4455 			smallStringt *s = getG(filters, rtSmallStringt, filter);
   4456 			if (eqG(s, STATUS_FILTER_STATES[DISABLE])) {
   4457 				pushG(status_filters, DISABLE);
   4458 			}
   4459 			else {
   4460 				pushG(status_filters, ENABLE);
   4461 			}
   4462 			finishG(s);
   4463 		}
   4464 		char *fc = appendG(filter, "_fgcolor");
   4465 		if (hasG(config_color_names, fc)) {
   4466 			smallStringt *s = getG(colors, rtSmallStringt, fc);
   4467 			smallArrayt *a  = splitG(s, ",");
   4468 
   4469 			// convert a strings to int
   4470 			enumerateSmallArray(a, CP, ci) {
   4471 				castS(cp, CP);
   4472 				setG(a, ci, parseIntG(cp));
   4473 				finishG(cp);
   4474 			}
   4475 			setNFreeG(status_fgColors, n, a);
   4476 			finishG(s);
   4477 		}
   4478 		free(fc);
   4479 		fc = appendG(filter, "_bgcolor");
   4480 		if (hasG(config_color_names, fc)) {
   4481 			smallStringt *s = getG(colors, rtSmallStringt, fc);
   4482 			smallArrayt *a  = splitG(s, ",");
   4483 
   4484 			// convert a strings to int
   4485 			enumerateSmallArray(a, CP, ci) {
   4486 				castS(cp, CP);
   4487 				setG(a, ci, parseIntG(cp));
   4488 				finishG(cp);
   4489 			}
   4490 			setNFreeG(status_bgColors, n, a);
   4491 			finishG(s);
   4492 		}
   4493 		free(fc);
   4494 	}
   4495 	//logVarG(status_fgColors);
   4496 	//logVarG(status_bgColors);
   4497 
   4498 	// Set text editor
   4499 	editor              = getNDupG(settings, rtChar,"editor");
   4500 
   4501 	// set user name and email
   4502 	char **settings_keys = keysG(settings);
   4503 	if (hasG(settings_keys, "username")) {
   4504 		user  = getNDupG(settings, rtChar, "username");
   4505 	}
   4506 	else {
   4507 		user  = strdup("Unknown");
   4508 	}
   4509 	if (hasG(settings, "useremail")) {
   4510 		email = getNDupG(settings, rtChar, "useremail");
   4511 	}
   4512 	else {
   4513 		emptyS(email);
   4514 	}
   4515 
   4516 	if (desktop) {
   4517 		smallStringt *s = getG(desktop, rtSmallStringt, "bookmarks");
   4518 		bookmarks = splitG(s, "|");
   4519 		finishG(s);
   4520 	}
   4521 	else {
   4522 		// no desktop section in ini file, empty bookmarks
   4523 		bookmarks = allocG(rtSmallArrayt);
   4524 	}
   4525 
   4526 	// init
   4527 	init();
   4528 
   4529 	listFreeManyS(data_keys, location_keys, settings_keys, config_filter_names, config_color_names);
   4530 	finishManyG(data, locations, filters, colors, settings, desktop);
   4531 }