💾 Archived View for gmi.noulin.net › gitRepositories › easydoneitCTui › file › edCore.c.gmi captured on 2023-01-29 at 13:15:46. Gemini links have been rewritten to link to archived content

View Raw

More Information

-=-=-=-=-=-=-

easydoneitCTui

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 }