easydoneitCTui

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

commit edf1011a3aa5e7340ab3b737fed461f055fa7b51
parent 74a3e04639bfc84e674a6602952fa605cfb2b54b
Author: Remy Noulin <loader2x@gmail.com>
Date:   Sun, 20 May 2018 00:08:05 +0200

easydoneit TUI

edCore.c        | 4531 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
edCore.h        |   71 +
edt.c           | 1996 ++++++++++++++++++++++++
package.yml     |   20 +
root.txt        |    1 +
tui/package.yml |   16 +
tui/tui.c       |   92 ++
tui/tui.h       |    7 +
8 files changed, 6734 insertions(+)

Diffstat:
AedCore.c | 4531+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AedCore.h | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aedt.c | 1996+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackage.yml | 20++++++++++++++++++++
Aroot.txt | 1+
Atui/package.yml | 16++++++++++++++++
Atui/tui.c | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atui/tui.h | 7+++++++
8 files changed, 6734 insertions(+), 0 deletions(-)

diff --git a/edCore.c b/edCore.c @@ -0,0 +1,4531 @@ +#include "libsheepyObject.h" +#include "shpPackages/ini/src/ini.h" +#include "edCore.h" + +/** @package edi_core + * Core module + */ + +/** 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. + * Values: 'cli', 'web' + */ +static char *user_interface = NULL; + +/** path to easydoneit.ini + * used in edi desktop + */ +static char *inipath = NULL; +smallDictt *ini = NULL; + +char *data_location = NULL; +static char *data_location_tasks = NULL; +static char *data_location_groups = NULL; +static char *data_location_tree = NULL; + +char *saved_data_location = NULL; + +/** Available databases */ +static smallArrayt *databases = NULL; + +/** list selected databases */ +static smallDictt *selected_d = NULL; +smallArrayt *selected = NULL; +static smallArrayt *selected_path = NULL; +/** default database where tasks are created */ +static char *default_add_in = NULL; + +/** add new tasks in 'bottom' of group or 'top' of group */ +char *add_top_or_bottom = NULL; + +/** Autlink groups (array of tids) */ +static smallArrayt *autolink = NULL; + +/** list of groups for edi ls -L, -La and -Lx (array of tids) */ +static smallArrayt *list_of_groups = NULL; + +/** characters for task ids */ +#define ID_BASE_DF ",0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz" +static char *ID_BASE = ID_BASE_DF; +static char *ID_BASE_STRING = ID_BASE_DF; +/** Length of order id in groups */ +#define ORDER_ID_LENGTH 8 +size_t BASE = sizeof(ID_BASE_DF)-1; + +char *TASK_STATUS[] = {" Active", + " Done", + " Ongoing", + " Pending", + "Inactive", + " Void", + NULL}; +char **TASK_STATUS_TRIM = NULL; + +/** Sort order for sort_task_attributes function, ongoing, active, pending, done, inactive, void */ +static u8 SORT_TASK_ORDER[] = {2,0,3,1,4,5}; + +/* TODO remove unused - static char *LIST_OPTIONS[] = {"tids", */ +/* "positions", */ +/* "html"}; */ +static char *list_option = "tids"; + +char *STATUS_FILTER_STATES[] = {"enable","disable"}; + +/** enables all status filters */ +smallArrayt *status_filters = NULL; + +/** status filter dictionary, initialized after loading ini file */ +smallDictt *status_filters_d= NULL; + +/** colors (default) */ +//static i16 no_color[4] = {-1,-1,-1,255}; +//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}}; +static smallArrayt *no_color = NULL; +static smallArrayt *status_fgColors = NULL; +static smallDictt *status_fgColors_d = NULL; +/** no background color by default */ +//static i16 status_bgColors[6][4] = {{-1,-1,-1,255}}; +static smallArrayt *status_bgColors = NULL; +static smallDictt *status_bgColors_d = NULL; + +/** text editor in terminal - easydoneit.ini [settings] EDITOR */ +static char *editor = "vi"; + +/** Agenda generated by edi.in_cli */ +static smallArrayt *agenda = NULL; + +/** user name */ +static char *user = NULL; + +/** user email */ +static char *email = NULL; + +/** bookmarks */ +smallArrayt *bookmarks = NULL; + +/** stats for statistics function */ +static smallDictt *stats = NULL; + +/** total number of tasks for statistics function */ +static u64 stats_total = 0; + +/** creation dates of tasks in statistics, used to find out oldest task */ +static smallArrayt *stats_creation_dates = NULL; + +/** timely state changes and creation dates + * Type: dict of dict of integers + * keys are dates + * each dates is a dictionary with STATS_OVERTIME_KEYS keys + * each key is the amount for states and creation + */ +static smallDictt *stats_overtime = NULL; + +/** timely state changes and creation dates */ +//STATS_OVERTIME_KEYS = [TASK_STATUS[i] for i in range(len(TASK_STATUS))] + ['Creation'] + +/* Group directory, cache result to speed up color functions */ +static smallArrayt *group_directory_file_list = NULL; + + + +static char *html_header = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">\n\ +<html>\n\ + <head>\n\ + <meta http-equiv=\"Content-Type\" content=\"text/html;charset=utf-8\">\n\ + <title>Tasks</title>\n\ + <style type=\"text/css\">\n\ + .subject {text-align: left}\n\ + .plannedStartDateTime {text-align: right}\n\ + .priority {text-align: right}\n\ + .inactive {color: #5E5E5E}\n\ + .late {color: #A020F0}\n\ + .active {color: #000000}\n\ + .duesoon {color: #FF8000}\n\ + .overdue {color: #FF0000}\n\ + .completed {color: #037700}\n\ +\n\ + body {\n\ + color: #333;\n\ + background-color: white;\n\ + font: 11px verdana, arial, helvetica, sans-serif;\n\ + }\n\ +\n\ + /* Styles for the title and table caption */\n\ + h1, caption {\n\ + text-align: center;\n\ + font-size: 18px;\n\ + font-weight: 900;\n\ + color: #778;\n\ + }\n\ +\n\ + /* Styles for the whole table */\n\ + #table {\n\ + border-collapse: collapse;\n\ + border: 2px solid #ebedff;\n\ + margin: 10px;\n\ + padding: 0;\n\ + }\n\ +\n\ + /* Styles for the header row */\n\ + .header {\n\ + font: bold 12px/14px verdana, arial, helvetica, sans-serif;\n\ + color: #07a;\n\ + background-color: #ebedff;\n\ + }\n\ +\n\ + /* Mark the column that is sorted on */\n\ + #sorted {\n\ + text-decoration: underline;\n\ + }\n\ +\n\ + /* Styles for a specific column */\n\ + .subject {\n\ + font-weight: bold;\n\ + }\n\ +\n\ + /* Styles for regular table cells */\n\ + td {\n\ + padding: 5px;\n\ + border: 2px solid #ebedff;\n\ + }\n\ +\n\ + /* Styles for table header cells */\n\ + th {\n\ + padding: 5px;\n\ + border: 2px solid #ebedff;\n\ + }\n\ +\n\ + </style>\n\ + </head>\n\ + <body>\n\ + <h1>Tasks</h1>\n\ + <table border=\"1\" id=\"table\">\n\ + <thead>\n\ + <tr class=\"header\">\n\ + <th class=\"subject\" scope=\"col\">Subject</th>\n\ + <th class=\"description\" scope=\"col\">Description</th>\n\ + </tr>\n\ + </thead>\n\ + <tbody>"; + +static char *html_footer = " </tbody>\n\ + </table>\n\ + </body>\n\ +</html>"; + + + + + +//////////// +// MACROS +//////////// + +/** + * generate task path in var + * tid is the task identity, should be declared in the context + */ +#define genTaskPath(var, path)\ + sprintf(var, "%s/" path, generate_task_path(tid)); + +/** + * declare var and generate path in easydoneit database + * var is a local string + * path is literal string representing a path in the task: "fgColor" + */ +#define createTaskPath(var, path)\ + char var[8192];\ + genTaskPath(var, path) + +//////////// +// MACROS END +//////////// + + + + +// git support +void keepDir(char *path) { + FILE *f; + f = fopen(path, "w"); + free(path); + fclose(f); +} + +int mkdirFree(char *path) { + int r; + r = mkdirParents(path); + free(path); + return r; +} + +int copyFree(char *s, char *d) { + int r; + r = copy(s, d); + freeManyS(s,d); + return r; +} + +/** initializes path to data. + * creates directories<br> + * creates root group and task<br> + * @ingroup EDI_CORE + */ +void init(void) { + // Set global variables + smallArrayt *ts = allocG(rtSmallArrayt); + fromArrayG(ts, TASK_STATUS, 0); + + status_filters_d = allocG(rtSmallDictt); + zipG(status_filters_d, ts, status_filters); + //logVarG(status_filters_d); + status_fgColors_d = allocG(rtSmallDictt); + zipG(status_fgColors_d, ts, status_fgColors); + //logVarG(status_fgColors_d); + status_bgColors_d = allocG(rtSmallDictt); + zipG(status_bgColors_d, ts, status_bgColors); + //logVarG(status_bgColors_d); + terminateG(ts); + + data_location_tasks = appendG(data_location, "/tasks"); + data_location_groups = appendG(data_location, "/groups"); + data_location_tree = appendG(data_location, "/tree"); + + if (not fileExistsG(data_location_tasks)) { + mkdirParentsG(data_location_tasks); + mkdirParentsG(data_location_groups); + mkdirParentsG(data_location_tree); + // git support + keepDir(appendG(data_location_tree, "/.keepDir")); + mkdirFree(appendG(data_location_groups, "/root")); + + // Create root description in tasks + char *task_path = appendG(data_location_tasks, "/root"); + mkdirParentsG(task_path); + // copy root description from script directory + char *d = shDirname(getProgPath()); + copyFree(appendG(d, "/root.txt"), appendG(task_path, "/description.txt")); + free(d); + + // Create status + char **l = NULL; + pushG(&l, TASK_STATUS[TASK_STATUS_VOID]); + char *tmp = appendG(task_path, "/status"); + writeFileG(l, tmp); + freeG(l); + free(task_path); + } + + // git support - when a new database is created, the group folder is empty and not saved in git + // create the group folder when the database is a cloned git with missing group folder + if (not fileExistsG(data_location_groups)) { + mkdirParentsG(data_location_groups); + } + if (not fileExistsG(data_location_tree)) { + mkdirParentsG(data_location_tree); + } +} + +/** log actions in database - data_location/log.txt + * @param[in] s string without user name and timestamp + * @ingroup EDI_CORE + */ +void edi_log(const char *s) { + char *fn = appendG(data_location, "/log.txt"); + FILE *f = fopen(fn, "a"); + if (!f) { + goto ret; + } + + char *sT = trimG(s); + // get current time + char *t = timeToS(time(0)); + fprintf(f, "%s - %s <%s> - %s\n", t, user, email, sT); + freeManyS(sT, t); + fclose(f); +ret: + free(fn); +} + +/** string returned from select_database */ +static char result_select_database[8192]; + +/** select location database + * @param[in] location database name + * @ingroup EDI_CORE + */ +const char *select_database(char *location) { + + if (hasG(selected_d, location)) { + freeManyS(data_location, data_location_tasks, data_location_groups, data_location_tree); + data_location = getNDupG(selected_d, rtChar, location); + + data_location_tasks = appendG(data_location, "/tasks"); + data_location_groups = appendG(data_location, "/groups"); + data_location_tree = appendG(data_location, "/tree"); + + if (not fileExists(data_location)) { + strcpy(result_select_database, data_location); + strcat(result_select_database, " is unreachable"); + } + else { + result_select_database[0] = 0; + } + } + else { + strcpy(result_select_database, location); + strcat(result_select_database, " is not found in the configuration (.easydoneit.ini)"); + } + return result_select_database; +} + +/** Mix foreground colors from task, groups and default colors. + * @return color array + * @param[in] tid task id + * @ingroup EDI_CORE + * Collect all defined colors in link groups. + * For each group, the group gets the first color defined in the tree<br> + * compute average color + */ +smallArrayt *mix_fgcolors(const char *tid) { + // mix colors + // collect all defined colors in groups to root + createAllocateSmallArray(colors); + createAllocateSmallArray(color); + // root has no parent group, dont search parent group color + if (not eqG(tid, "root")) { + // find all groups - link and parent group + char taskPath[8192]; + createAllocateSmallArray(groups); + if (is_linked(tid)) { + genTaskPath(taskPath, "groups/"); + groups = readDirG(rtSmallArrayt, taskPath); + } + else { + pushG(groups, (char*)find_group_containing_task(tid)); + } + + // walk parent groups until a color or root is found + while (lenG(groups)) { + enum {search, found_color}; + int status_color = search; + sprintf(taskPath, "%s/fgColor", generate_task_path(getG(groups, rtChar, 0))); + if (fileExists(taskPath)) { + createAllocateSmallString(f); + readFileG(f, taskPath); + smallArrayt *c = splitG(f, ","); + // convert c strings to int + enumerateSmallArray(c, CP, ci) { + castS(cp, CP); + setG(c, ci, parseIntG(cp)); + finishG(cp); + } + terminateG(f); + // -1 means no_color, mix colors + if (getG(c, rtI64 ,0) != -1) { + pushNFreeG(colors, c); + status_color = found_color; + } + else { + // c is not a color + terminateG(c); + } + } + if (status_color == search) { + char *parent_group = (char *) find_group_containing_task(getG(groups, rtChar, 0)); + if (eqG(parent_group, "error")) { + // break infinite loop when there is an error in the database + break; + } + if (not eqG(parent_group, "root")) { + pushG(groups, parent_group); + } + } + delG(groups, 0, 1); + } + terminateG(groups); + + // compute average color + if (lenG(colors)) { + pushG(color, 0);pushG(color, 0);pushG(color, 0);pushG(color, 0); + forEachSmallArray(colors, C) { + cast(smallArrayt*, c, C); + range(i, lenG(c)) { + *getG(color, rtI32P, i) += getG(c, rtI32, i); + } + finishG(C); + } + size_t colorCount = lenG(colors); + range(i, lenG(color)) { + *getG(color, rtI32P, i) /= colorCount; + } + } + } + if (not lenG(colors)) { + // no defined colors, use default color for status + char *task_status = (char *)get_status(tid); + terminateG(color); + color = getNDupG(status_fgColors_d, rtSmallArrayt, task_status); + } + terminateG(colors); + return color; +} + +/** get color for task tid + * @return color array + *# @param[in] tid task id + * @ingroup EDI_CORE + * when no color is set, mix colors from groups or use defaults + */ +smallArrayt *get_forground_color(const char *tid) { + //color = no_color + smallArrayt *color = NULL; + createTaskPath(fgPath, "fgColor"); + + if (fileExists(fgPath)) { + createAllocateSmallArray(color); + createAllocateSmallString(f); + readFileG(f, fgPath); + smallArrayt *c = splitG(f, ","); + // convert c strings to int + forEachSmallArray(c, CP) { + castS(cp, CP); + pushG(color, parseIntG(cp)); + finishG(cp); + } + terminateManyG(f, c); + // -1 means no_color, mix colors + if (getG(color, rtI64 ,0) != -1) { + terminateG(color); + color = mix_fgcolors(tid); + } + } + else { + color = mix_fgcolors(tid); + } + if (not color) { + color = dupG(no_color); + } + return color; +} + +/** set color for task tid + * @param[in] tid task id + * @param[in] color_s color array + * @ingroup EDI_CORE + */ +void set_forground_color(const char *tid , const char *color_s) { + createTaskPath(fgPath, "fgColor"); + writeFileG(color_s, fgPath); + sprintf(fgPath, "set foreground color in %s to %s", tid,color_s); + edi_log(fgPath); +} + +/** remove foreground color for task tid + * @param[in] tid task id + * @ingroup EDI_CORE + */ +void remove_foreground_color(const char *tid) { + createTaskPath(fgPath, "fgColor"); + if (fileExists(fgPath)) { + rmAllG(fgPath); + } +} + +/** Mix background colors from task, groups and default colors. + * @return color array + * @param[in] tid task id + * @ingroup EDI_CORE + * Collect all defined colors in link groups. + * For each group, the group gets the first color defined in the tree<br> + * compute average color + */ +smallArrayt *mix_bgcolors(const char *tid) { + // mix colors + // collect all defined colors in groups to root + createAllocateSmallArray(colors); + createAllocateSmallArray(color); + // root has no parent group, dont search parent group color + if (not eqG(tid, "root")) { + // find all groups - link and parent group + char taskPath[8192]; + createAllocateSmallArray(groups); + if (is_linked(tid)) { + genTaskPath(taskPath, "groups/"); + groups = readDirG(rtSmallArrayt, taskPath); + } + else { + pushG(groups, (char*)find_group_containing_task(tid)); + } + + // walk parent groups until a color or root is found + while (lenG(groups)) { + enum {search, found_color}; + int status_color = search; + sprintf(taskPath, "%s/bgColor", generate_task_path(getG(groups, rtChar, 0))); + if (fileExists(taskPath)) { + createAllocateSmallString(f); + readFileG(f, taskPath); + smallArrayt *c = splitG(f, ","); + // convert c strings to int + enumerateSmallArray(c, CP, ci) { + castS(cp, CP); + setG(c, ci, parseIntG(cp)); + finishG(cp); + } + terminateG(f); + // -1 means no_color, mix colors + if (getG(c, rtI64 ,0) != -1) { + pushNFreeG(colors, c); + status_color = found_color; + } + else { + // c is not a color + terminateG(c); + } + } + if (status_color == search) { + char *parent_group = (char *) find_group_containing_task(getG(groups, rtChar, 0)); + if (eqG(parent_group, "error")) { + // break infinite loop when there is an error in the database + break; + } + if (not eqG(parent_group, "root")) { + pushG(groups, parent_group); + } + } + delG(groups, 0, 1); + } + terminateG(groups); + + // compute average color + if (lenG(colors)) { + pushG(color, 0);pushG(color, 0);pushG(color, 0);pushG(color, 0); + forEachSmallArray(colors, C) { + cast(smallArrayt*, c, C); + range(i, lenG(c)) { + *getG(color, rtI32P, i) += getG(c, rtI32, i); + } + finishG(C); + } + size_t colorCount = lenG(colors); + range(i, lenG(color)) { + *getG(color, rtI32P, i) /= colorCount; + } + } + } + if (not lenG(colors)) { + // no defined colors, use default color for status + char *task_status = (char *)get_status(tid); + terminateG(color); + color = getNDupG(status_bgColors_d, rtSmallArrayt, task_status); + } + return color; +} + +/** get color for task tid + * @return color array + * @param[in] tid task id + * @ingroup EDI_CORE + * when no color is set, mix colors from groups or use defaults + */ +smallArrayt *get_background_color(const char *tid) { + //color = no_color + smallArrayt *color = NULL; + createTaskPath(bgPath, "bgColor"); + + if (fileExists(bgPath)) { + createAllocateSmallArray(color); + createAllocateSmallString(f); + readFileG(f, bgPath); + smallArrayt *c = splitG(f, ","); + // convert c strings to int + forEachSmallArray(c, CP) { + castS(cp, CP); + pushG(color, parseIntG(cp)); + finishG(cp); + } + terminateManyG(f, c); + // -1 means no_color, mix colors + if (getG(color, rtI64 ,0) != -1) { + terminateG(color); + color = mix_bgcolors(tid); + } + } + else { + color = mix_bgcolors(tid); + } + if (not color) { + color = dupG(no_color); + } + return color; +} + +/** set color for task tid + * @return color array + * @param[in] tid task id + * @param[in] color_s color array + * @ingroup EDI_CORE + */ +void set_background_color(const char *tid, const char *color_s) { + createTaskPath(bgPath, "bgColor"); + writeFileG(color_s, bgPath); + sprintf(bgPath, "set background color in %s to %s", tid,color_s); + edi_log(bgPath); +} + +/** remove background color for task tid + * @param[in] tid task id + * @ingroup EDI_CORE + */ +void remove_background_color(const char *tid) { + createTaskPath(bgPath, "bgColor"); + if (fileExists(bgPath)) { + rmAllG(bgPath); + } +} + +/** string returned from color_to_hex FF22AA */ +static char return_color_to_hex[7]; + +/** converts color to hex format + * @return color hex string + * @param[in] color color array + * @ingroup EDI_CORE + */ +const char *color_to_hex(smallArrayt *color) { + createAllocateSmallArray(c); + forEachSmallArray(color, N) { + cast(smallIntt *, n, N); + if (getG(n, rtI64, unusedV) == -1) { + // -1 is no color, white + pushG(c, 255); + } + else { + pushNFreeG(c, dupG(n)); + } + finishG(n); + } + sprintf(return_color_to_hex, "%02x%02x%02x", getG(c, rtI64, 0), getG(c, rtI64, 1), getG(c, rtI64, 2)); + terminateG(c); + return return_color_to_hex; +} + + +/** string returned from generate_task_path */ +static char result_generate_task_path[8192]; + +/** filesystem path for task in database tasks + * @return path + * @param[in] tid task id + * @ingroup EDI_CORE + */ +const char *generate_task_path(const char *tid) { + sprintf(result_generate_task_path, "%s/%s", data_location_tasks, tid); + return result_generate_task_path; +} + +/** string returned from generate_group_path */ +static char result_generate_group_path[8192]; + +/** filesystem path for group in database groups + * @return path + * @param[in] tid task id + * @ingroup EDI_CORE + */ +const char *generate_group_path(const char *tid) { + sprintf(result_generate_group_path, "%s/%s/", data_location_groups, tid); + return result_generate_group_path; +} + + +static char return_get_status[64]; + +/** get status for tid + * @return status string + * @param[in] tid task id + * @ingroup EDI_CORE + */ +const char *get_status(const char *tid) { + // open status file + createTaskPath(statusPath, "status"); + createAllocateSmallString(status); + if (!readFileG(status, statusPath)) { + goto except; + } + if (lenG(status) > 63) { + // the status is too long, this is wrong + goto except; + } + strcpy(return_get_status, ssGet(status)); + terminateG(status); + return return_get_status; +except: + printf("%s is an invalid task.", generate_task_path(tid)); + return TASK_STATUS[TASK_STATUS_VOID]; +} + +/** returns s + * @return s + * @param[in] tid task id + * @param[in] s task title string + * @ingroup EDI_CORE + * Replaces generate_task_string_with_tid in GUI + */ +smallStringt *passThroughTitle(const char *tid, smallStringt *s) { + return dupG(s); +} + +/** creates a string with task id, status and string parameter + * @return string + * @param[in] tid task id + * @param[in] s task title string + * @ingroup EDI_CORE + * Displays a task depending on edi_core.list_option + */ +smallStringt *generate_task_string_with_tid(const char *tid, smallStringt *s) { + const char *status = get_status(tid); + smallStringt *r = NULL; + if (eqG(list_option, "tids")) { + // %*s to dynamically adjust id length + r = formatO("%" stringifyExpr(ID_LENGTH) "s - %s - %s", tid, status, ssGet(s)); + } + if (eqG(list_option, "positions")) { + if (is_this_task_a_group(tid)) { + // print tid for groups in the list + r = formatO("%" stringifyExpr(ID_LENGTH) "s - %s - %s", tid, status, ssGet(s)); + } + else { + // hide tids for normal tasks + r = formatO("%" stringifyExpr(ID_LENGTH) "s - %s - %s", "", status, ssGet(s)); + } + } + if (eqG(list_option, "md")) { + // print title and description + createTaskPath(taskPath, "description.txt"); + createAllocateSmallArray(description); + readFileG(description, taskPath); + // remove title line + delG(description, 0 ,1); + + // add <br> for long titles + smallStringt *j = joinG(description, "\n"); + r = formatO("\n---\n#%s<br>\n%s", ssGet(s), ssGet(j)); + terminateManyG(description, j); + } +/* if list_option = 'rst' */ +/* # print title and description */ +/* f = open generate_task_path(tid)+os.sep+'description.txt' */ +/* # remove title line */ +/* description_l = f readlines[1:] */ +/* f close */ +/* */ +/* # convert urls to link */ +/* NOBRACKET = r'[^\]\[]*' */ +/* BRK = ( r'\[(' */ +/* + (NOBRACKET + r'(\[')*6 */ +/* + (NOBRACKET+ r'\])*')*6 */ +/* + NOBRACKET + r')\]' ) */ +/* NOIMG = r'(?<!\!)' */ +/* LINK_RE = NOIMG + BRK + \ */ +/* r'''\(\s*(<.*?>|((?:(?:\(.*?\))|[^\(\)]))*?)\s*((['"])(.*?)\12\s*)?\)''' */ +/* # [text](url) or [text](<url>) or [text](url "title") */ +/* */ +/* compiled_re = re.compile("^(.*?)%s(.*?)$" % LINK_RE, re.DOTALL | re.UNICODE) */ +/* */ +/* for line_index,l in enumerate(description_l) */ +/* if 'http' in l */ +/* try */ +/* match = compiled_re.match(l) */ +/* url = match.group(9) */ +/* text = match.group(2) */ +/* before_url = match.group(1) */ +/* #title = match.group(13) */ +/* after_url = match.group(14) */ +/* */ +/* # added a space after text because rst complains when there is no space before < */ +/* # delimiting the url */ +/* l = '%s`%s <%s>`_%s'% (before_url, text, url, after_url) */ +/* except */ +/* # not used, information */ +/* link_status = 'WARNING: the link is not in format [title](url)' */ +/* description_l[line_index] = l */ +/* */ +/* description = ''.join(description_l) */ +/* */ +/* tid = ' ' */ +/* # remove position and type (GROUP, LINK) from title */ +/* title = s[11:] lstrip */ +/* r = '%s\n'%title + '='*len(title) + '\n%s\n\n'%description */ +/* if list_option = 'html' */ +/* #<tr bgcolor="#FEFFCC"> */ +/* # <td class="subject"> <font color="#000000">t1</font></td> */ +/* # <td class="description"> <font color="#000000">&nbsp;</font></td> */ +/* #</tr> */ +/* #:define read_description */ +/* # convert < to &lt; and > &gt; to ignore html tags in title line */ +/* s = s.replace('<','&lt;').replace('>','&gt;') */ +/* f = open generate_task_path(tid)+os.sep+'description.txt' */ +/* # remove title line, convert < to &lt; and > &gt; to ignore html tags */ +/* description_l = ['%s<br>'%i rstrip.replace('<','&lt;').replace('>','&gt;') for i in f readlines[1:]] */ +/* f close */ +/* */ +/* # convert urls to link */ +/* NOBRACKET = r'[^\]\[]*' */ +/* BRK = ( r'\[(' */ +/* + (NOBRACKET + r'(\[')*6 */ +/* + (NOBRACKET+ r'\])*')*6 */ +/* + NOBRACKET + r')\]' ) */ +/* NOIMG = r'(?<!\!)' */ +/* LINK_RE = NOIMG + BRK + \ */ +/* r'''\(\s*(<.*?>|((?:(?:\(.*?\))|[^\(\)]))*?)\s*((['"])(.*?)\12\s*)?\)''' */ +/* # [text](url) or [text](<url>) or [text](url "title") */ +/* */ +/* compiled_re = re.compile("^(.*?)%s(.*?)$" % LINK_RE, re.DOTALL | re.UNICODE) */ +/* */ +/* for line_index,l in enumerate(description_l) */ +/* if 'http' in l */ +/* try */ +/* match = compiled_re.match(l) */ +/* url = match.group(9) */ +/* text = match.group(2) */ +/* before_url = match.group(1) */ +/* #title = match.group(13) */ +/* after_url = match.group(14) */ +/* */ +/* l = '%s<a href="%s">%s</a>%s'% (before_url, url, text, after_url) */ +/* except */ +/* # not used, information */ +/* link_status = 'WARNING: the link is not in format [title](url)' */ +/* description_l[line_index] = l */ +/* */ +/* description = '\n'.join(description_l) */ +/* #:end */ +/* */ +/* if is_this_task_a_group(tid) and user_interface = 'web' */ +/* subject = '<a href="edi_web.py?tid=%s">%s - %s</a>'%(tid,status,s) */ +/* else */ +/* subject = '%s - %s'%(status,s) */ +/* fg = color_to_hex(get_forground_color(tid)) */ +/* bg = color_to_hex(get_background_color(tid)) */ +/* 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) */ + return r; +} + +/** creates a string with task id, status and string parameter + * @return list of strings + * @param[in] tid task id + * @param[in] s task title string + * @ingroup EDI_CORE + * Displays a group depending on edi_core.list_option + */ +smallStringt *generate_group_string_with_tid (const char *tid, smallStringt *s) { + const char *status = get_status(tid); + smallStringt *r = NULL; + if ((eqG(list_option, "tids")) or (eqG(list_option, "positions"))) { + // %*s to dynamically adjust id length + r = formatO("%" stringifyExpr(ID_LENGTH) "s - %s - %s", tid, status, ssGet(s)); + } +/* if list_option = 'md' */ +/* tid = ' ' */ +/* # print group title only */ +/* r = '#%s'%s.replace(' 0 - GROUP','') */ +/* if agenda */ +/* r += '\n---\n# Agenda\n%s' % '\n\n'.join(agenda) */ +/* if list_option = 'rst' */ +/* tid = ' ' */ +/* # print group title only */ +/* title = s.replace(' 0 - GROUP','') */ +/* r = '='*len(title) + '\n%s\n'%title + '='*len(title) + '\n\n' */ +/* if agenda */ +/* r += 'Agenda\n======\n%s\n\n' % '\n'.join(agenda) */ +/* else */ +/* r += '\n.. contents::\n\n' */ +/* if list_option = 'html' */ +/* #<tr bgcolor="#FEFFCC"> */ +/* # <td class="subject"> <font color="#000000">t1</font></td> */ +/* # <td class="description"> <font color="#000000">&nbsp;</font></td> */ +/* #</tr> */ +/* # display description in html */ +/* #:read_description */ +/* */ +/* subject = '%s - %s'%(status, s) */ +/* fg = color_to_hex(get_forground_color(tid)) */ +/* bg = color_to_hex(get_background_color(tid)) */ +/* 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) */ + return r; +} + +/** determines if task is a group + * @return empty string or 'this task is a group' + * @param[in] tid task id + * @ingroup EDI_CORE + */ +bool is_this_task_a_group(const char *tid) { + // Identify if task is a group + bool is_a_group = false; + // list groups + smallArrayt *groups = readDirDirG(rtSmallArrayt, data_location_groups); + if (binarySearchG(groups, tid) != -1) { + is_a_group = true; + } + terminateG(groups); + return is_a_group; +} + +/** get task title + * @return first line of task description + * @param[in] tid task id + * @ingroup EDI_CORE + */ +smallStringt *get_task_title(const char *tid) { + createTaskPath(task_path, "description.txt"); + createAllocateSmallArray(f); + readFileG(f, task_path); + smallStringt *r = getNDupG(f, rtSmallStringt, 0); + terminateG(f); + if (not r) { + // empty description, return empty string + r = allocG(""); + } + return r; +} + +/** get creation date + * @return array of date string and unix time + * @param[in] tid task id + * @ingroup EDI_CORE + */ +smallArrayt *get_creation_date(const char *tid) { + createAllocateSmallArray(r); + // Figure out creation time and modification times for description and status + createTaskPath(task_path, "ctime.txt"); + if (not fileExists(task_path)) { + pushG(r, "Not available"); + pushG(r, 0); + } + else { + createAllocateSmallString(task_ctime); + readFileG(task_ctime, task_path); + pushG(r, task_ctime); + struct tm tm; + if (not strptime(ssGet(task_ctime), "%Y-%m-%d %H:%M", &tm)) { + // string time failed to parse + pushG(r, 0); + } + else { + time_t t = mktime(&tm); + pushG(r, (i64) t); + } + finishG(task_ctime); + } + return r; +} + +/** get media + * @return media file name + * @param[in] tid task id + * @ingroup EDI_CORE + */ +smallArrayt *get_media(const char *tid) { + createAllocateSmallArray(r); + createTaskPath(task_path, "media"); + if (fileExists(task_path)) { + smallArrayt *files = readDirG(rtSmallArrayt, task_path); + smallStringt *fn = getG(files, rtSmallStringt, 0); + smallArrayt *spl = splitG(fn, "."); + pushG(r, getG(spl, rtChar, 0)); + pushNFreeG(r, catS(task_path, "/", getG(files, rtChar, 0))); + terminateManyG(files, fn, spl); + } + else { + pushG(r, "None"); + } + return r; +} + +/** remove media + * @param[in] tid task id + * @ingroup EDI_CORE + */ +void remove_media(const char *tid) { + createTaskPath(task_path, "media"); + if (fileExists(task_path)) { + rmAll(task_path); + } +} + + +/** set media, one media file per task + * @return media array [media type (sound, image), file name] + * @param[in] tid task id + * @param[in] filename media file to copy to task + * @ingroup EDI_CORE + */ +smallArrayt *set_media(const char *tid, const char *filename) { + createAllocateSmallArray(r); + + createTaskPath(task_path, "media"); + char *fn = trimG(filename); + if (endsWithG(fn, "wav")) { + //#:define create_media_folder + if (not fileExists(task_path)) { + // create media folder + mkdirParentsG(task_path); + } + //#:end + else { + // media exists, check type + smallArrayt *files = readDirG(rtSmallArrayt, task_path); + if (not endsWithG(getG(files, rtChar, 0), "wav")) { + smallStringt *s = formatO("%s is not a sound.", tid); + pushNFreeG(r, s); + terminateG(files); + goto end; + } + terminateG(files); + } + // sound file + char fp[8192]; + sprintf(fp, "%s/sound.wav", task_path); + copy(filename, fp); + pushG(r, "sound.wav"); + } + if (endsWithG(fn, "jpg")) { + //#:create_media_folder + if (not fileExists(task_path)) { + // create media folder + mkdirParentsG(task_path); + } + else { + // media exists, check type + smallArrayt *files = readDirG(rtSmallArrayt, task_path); + if (not endsWithG(getG(files, rtChar, 0), "jpg")) { + smallStringt *s = formatO("%s is not an image.", tid); + pushNFreeG(r, s); + terminateG(files); + goto end; + } + terminateG(files); + } + // image file + char fp[8192]; + sprintf(fp, "%s/image.jpg", task_path); + copy(filename, fp); + pushG(r, "image.jpg"); + } +end: + free(fn); + if (not lenG(r)) { + smallStringt *s = formatO("%s is not a supported media file.", filename); + pushNFreeG(r, s); + } + else { + char log[8192]; + sprintf(log, "added media %s in task %s", filename, tid); + edi_log(log); + } + return r; +} + +/** get attachments + * @return attachment file names + * @param[in] tid task id + * @ingroup EDI_CORE + */ +smallArrayt *get_attachments(const char *tid) { + smallArrayt *r; + createTaskPath(task_path, "attachments/"); + if (fileExists(task_path)) { + // list files in attachments folder and prepend task_path + // r holds the paths to the attachments + r = readDirAllG(rtSmallArrayt, task_path); + enumerateSmallArray(r, F, i) { + castS(f, F); + prependG(f, task_path); + setNFreePG(r, i, f); + } + } + else { + initiateG(&r); + pushG(r, "None"); + } + return r; +} + +/** remove attachments + * @param[in] tid task id + * @ingroup EDI_CORE + */ +void remove_attachments(const char *tid) { + createTaskPath(task_path, "attachments"); + if (fileExists(task_path)) { + rmAll(task_path); + } +} + + +/** set attachments + * @return attachment file names + * @param[in] tid task id + * @param[in] array of filenames + * @ingroup EDI_CORE + * + * With *, set_attachments copies multiple files at once + */ +smallArrayt *set_attachments(const char *tid, smallArrayt *filenames) { + createTaskPath(task_path, "attachments/"); + if (not fileExists(task_path)) { + // create attachment folder + mkdirParentsG(task_path); + } + + createAllocateSmallArray(r); + forEachSmallArray(filenames, FN) { + castS(fn, FN); + if (not copyG(fn,task_path)) goto except; + char log[8192]; + sprintf(log, "added attachment %s in task %s", ssGet(fn), tid); + edi_log(log); + // Remove path from fn, keep only filename + char *s = basename(ssGet(fn)); + pushNFreeG(r, appendG(task_path, s)); + goto end; + except: + pushNFreeG(r, appendG("Failed to copy ", ssGet(fn))); + end: + finishG(fn); + } + return r; +} + +/** sort task attributes (ls in edi cli) + * @return sorted task list by state + * @param[in] result from list_group + * @ingroup EDI_CORE + */ +smallArrayt *sort_task_attributes(smallArrayt *task_attributes) { + createAllocateSmallArray(r); + // Keep head group on top + smallDictt *d = getG(task_attributes, rtSmallDictt, 0); + if (eqG(getG(d, rtChar, "head"), "head group")) { + pushNFreeG(r, dupG(d)); + delG(task_attributes, 0, 1); + } + finishG(d); + range(s, COUNT_ELEMENTS(SORT_TASK_ORDER)) { + forEachSmallArray(task_attributes, T) { + cast(smallDictt *, t, T); + if (eqG(getG(t, rtChar, "status"), TASK_STATUS[s])) { + pushG(r, T); + } + finishG(T); + } + } + return r; +} + +/** sort function for sorting an array of tasks by date from newest to oldest + * @return compare result + * @param[in] result from list_group + * @ingroup EDI_CORE + */ +int sortdatefunc(smallDictt *x, smallDictt *y) { + i64 xv = getG(x, rtI64, "ctime"); + i64 yv = getG(y, rtI64, "ctime"); + + if (xv == yv) return 0; + if (yv > xv) return 1; + else return -1; +} + +/** sort task attributes (ls in edi cli) + * @return sorted task list by date + * @param[in] result from list_group + * @ingroup EDI_CORE + */ +smallArrayt *sort_task_attributes_by_date(smallArrayt *task_attributes) { + createAllocateSmallArray(r); + // Keep head group on top + smallDictt *d = getG(task_attributes, rtSmallDictt, 0); + if (eqG(getG(d, rtChar, "head"), "head group")) { + pushNFreeG(r, dupG(d)); + delG(task_attributes, 0, 1); + } + finishG(d); + // -1 to exclude the empty line + smallArrayt *a = copyRngG(task_attributes, 0, -1); + //TODO a.sort(sortdatefunc) + //print task_attributes + appendNSmashG(r, a); + pushNFreeG(r, getG(task_attributes, rtBaset, -1)); + return r; +} + +/** Get task item in list_group format + * @return r dictionary representing a task like the items returned by the list_group function + * @param[in] tid task id + * @ingroup EDI_CORE + * for edi desktop + */ +smallDictt *get_task_in_list_group_format(const char *tid) { + createAllocateSmallDict(r); + setG(r, "head", "element"); + setG(r, "tid", tid); + char *group = " "; + if (is_this_task_a_group(tid)) { + group = "GROUP"; + } + if (is_linked(tid)) { + group = " LINK"; + } + setG(r, "group", group); + // figure out position in group + i64 current_position = -1; + const char *group_path = generate_group_path(find_group_containing_task(tid)); + smallArrayt *lGroups = readDirDirG(rtSmallArrayt, group_path); + enumerateSmallArray(lGroups, FN, n) { + castS(fn, FN); + if (hasG(fn, tid)) { + // found tid in list + // position in list + smallStringt *posS = copyRngG(fn, 0 , ORDER_ID_LENGTH); + current_position = baseconvert_to_dec(ssGet(posS)); + terminateG(posS); + break; + } + finishG(fn); + } + setG(r, "position", current_position); + // store title line + createTaskPath(task_path, "description.txt"); + FILE *f = fopen(task_path, "r"); + setNFreeG(r, "title", trimG(readLineG(rtSmallStringt, f))); + fclose(f); + setG(r, "status", get_status(tid)); + smallArrayt *cd = get_creation_date(tid); + setG(r, "ctime", getG(cd, rtI64, 1)); + terminateManyG(lGroups, cd); + return r; +} + +/** list id - status - group - first line from description + * @return list of tasks and groups title + * @param[in] tid task id + * @ingroup EDI_CORE + * items in groups have the format 'ORDER_ID''TASK_ID'<br> + * task 0 is group tittle<br> + *<br> + * result array elements:<br> + * {head :string, tid :string, position :value, group :string, title :string, status :string}<br> + * head tells if the element is a group title.<br> + *<br> + * example:<br> + *fTTB1KRWfDpoSR1_ - Active - GROUP Motivational Interviewing<br> + *62ZvFA_q0pCZFr0Y - Active - news<br> + */ +smallArrayt *list_group(const char *tid) { + createAllocateSmallArray(result); + // list visible groups only + const char *task_status = get_status(tid); + if (getG(status_filters_d, rtI32, task_status) == ENABLE) { + // List task titles in group + const char *group_path = generate_group_path(tid); + + // List groups to indicate when a task is a group + // Identify if task is a group + smallArrayt *groups = readDirDirG(rtSmallArrayt, data_location_groups); + // End Identify if task is a group + + // print items in tid in order + smallArrayt *tasksInGroup = readDirG(rtSmallArrayt, group_path); + enumerateSmallArray(tasksInGroup, FN, n) { + castS(fnp, FN); + smallStringt *fn = allocG(basename(ssGet(fnp))); + if (lenG(fn) != ORDER_ID_LENGTH+ID_LENGTH) { + // verify filename format in groups, cifs creates temporary files, ignore them + printf("EDI_ERROR: Database inconsistent, delete file - rm %s%s\n", group_path, ssGet(fn)); + } + else { + // print position in list + smallStringt *posS = copyRngG(fn, 0 , ORDER_ID_LENGTH); + i64 current_position = baseconvert_to_dec(ssGet(posS)); + terminateG(posS); + + // Identify if task is a group + // Remove order_id, keep task id only + char *group = " "; + if (hasG(groups, ssGet(fn)+ORDER_ID_LENGTH)) { + group = "GROUP"; + } + if (is_linked(ssGet(fn)+ORDER_ID_LENGTH)) { + group = " LINK"; + } + // End Identify if task is a group + // Get task title + // Remove order_id, keep task id only + char task_path[8192]; + sprintf(task_path, "%s/description.txt", generate_task_path(ssGet(fn)+ORDER_ID_LENGTH)); + FILE *f = fopen(task_path, "r"); + // Remove order_id, keep task id only + const char *task_status = get_status(ssGet(fn)+ORDER_ID_LENGTH); + if ((not n) and (not eqG(tid, "root"))) { + // First task is group title + // filter status, keep task if status filter is enabled + if (getG(status_filters_d, rtI32, task_status) == ENABLE) { + createAllocateSmallDict(task_d); + setG(task_d, "head", "head group"); + setG(task_d, "tid", ssGet(fn)+ORDER_ID_LENGTH); + setG(task_d, "position", current_position); + setG(task_d, "group", group); + setNFreeG(task_d, "title", trimG(readLineG(rtSmallStringt, f))); + setG(task_d, "status", get_status(ssGet(fn)+ORDER_ID_LENGTH)); + smallArrayt *cd = get_creation_date(ssGet(fn)+ORDER_ID_LENGTH); + setG(task_d, "ctime", getG(cd, rtI64, 1)); + terminateG(cd); + pushNFreeG(result, task_d); + } + } + else { + // filter status, keep task if status filter is enabled + if (getG(status_filters_d, rtI32, task_status) == ENABLE) { + createAllocateSmallDict(task_d); + setG(task_d, "head", "element"); + setG(task_d, "tid", ssGet(fn)+ORDER_ID_LENGTH); + setG(task_d, "position", current_position); + setG(task_d, "group", group); + setNFreeG(task_d, "title", trimG(readLineG(rtSmallStringt, f))); + setG(task_d, "status", get_status(ssGet(fn)+ORDER_ID_LENGTH)); + smallArrayt *cd = get_creation_date(ssGet(fn)+ORDER_ID_LENGTH); + setG(task_d, "ctime", getG(cd, rtI64, 1)); + terminateG(cd); + pushNFreeG(result, task_d); + } + } + fclose(f); + } + terminateG(fn); + finishG(fnp); + } + terminateManyG(groups, tasksInGroup); + createAllocateSmallDict(task_d); + setG(task_d, "head", "empty line"); + setG(task_d, "tid", ""); + setG(task_d, "position", 0); + setG(task_d, "group", ""); + setG(task_d, "title", ""); + setG(task_d, "status", ""); + setG(task_d, "ctime", 0); + pushNFreeG(result, task_d); + } + return result; +} + +smallArrayt *listBookmarks(void) { + createAllocateSmallArray(result); + + // check if there are bookmarks + if (not eqG(getG(bookmarks, rtChar, 0), "")) { + + enumerateSmallArray(bookmarks, FN, n) { + castS(fn, FN); + + // print position in list + i64 current_position = n; + + // setup database + save_edi_core_data_location(); + setup_data_location_for_tid(ssGet(fn)); + + // Identify if task is a group + // Remove order_id, keep task id only + char *group = " "; + if (is_this_task_a_group(ssGet(fn))) { + group = "GROUP"; + } + if (is_linked(ssGet(fn))) { + group = " LINK"; + } + // End Identify if task is a group + // Get task title + char task_path[8192]; + sprintf(task_path, "%s/description.txt", generate_task_path(ssGet(fn))); + FILE *f = fopen(task_path, "r"); + const char *task_status = get_status(ssGet(fn)); + // filter status, keep task if status filter is enabled + if (getG(status_filters_d, rtI32, task_status) == ENABLE) { + createAllocateSmallDict(task_d); + setG(task_d, "head", "element"); + setG(task_d, "tid", ssGet(fn)); + setG(task_d, "position", current_position); + setG(task_d, "group", group); + setNFreeG(task_d, "title", trimG(readLineG(rtSmallStringt, f))); + setG(task_d, "status", get_status(ssGet(fn))); + smallArrayt *cd = get_creation_date(ssGet(fn)); + setG(task_d, "ctime", getG(cd, rtI64, 1)); + terminateG(cd); + pushNFreeG(result, task_d); + } + fclose(f); + restore_edi_core_data_location(); + finishG(fn); + } + } + createAllocateSmallDict(task_d); + setG(task_d, "head", "empty line"); + setG(task_d, "tid", ""); + setG(task_d, "position", 0); + setG(task_d, "group", ""); + setG(task_d, "title", ""); + setG(task_d, "status", ""); + setG(task_d, "ctime", 0); + pushNFreeG(result, task_d); + return result; +} + +/** lists all items in the tid group + * @return list of tasks and groups title + * @param[in] tid task id + * @ingroup EDI_CORE + */ +smallArrayt *list_tree(const char *tid) { + createAllocateSmallArray(result); + // walk_group is the list of groups to visit. FIFO + createAllocateSmallArray(walk_group); + pushG(walk_group, tid); + // the while loop goes through all the group that are found + while (lenG(walk_group)) { + // list visible groups only + const char *task_status = get_status(getG(walk_group, rtChar, 0)); + if (getG(status_filters_d, rtI32, task_status) == ENABLE) { + // list items in first group + smallArrayt *tasks = readDirG(rtSmallArrayt, generate_group_path(getG(walk_group, rtChar, 0))); + appendNSmashG(result, list_group(getG(walk_group, rtChar, 0))); + + // add group found in first group + forEachSmallArray(tasks, T) { + castS(t, T); + // Check tasks that are not title task in a group + if (not eqG(ssGet(t)+ORDER_ID_LENGTH, getG(walk_group, rtChar, 0)) and is_this_task_a_group(ssGet(t)+ORDER_ID_LENGTH)) { + pushG(walk_group, ssGet(t)+ORDER_ID_LENGTH); + } + finishG(t); + } + terminateG(tasks); + } + + // remove first group to list items in next group + delG(walk_group, 0, 1); + } + terminateG(walk_group); + return result; +} + +/** string returned from generate_id */ +static char return_generate_id[ID_LENGTH+1]; + +/** generates a random task id + * @return new task id + * @ingroup EDI_CORE + */ +const char *generate_id(void) { + return_generate_id[ID_LENGTH] = 0; + + randomUrandomOpen(); + range(i, ID_LENGTH) { + return_generate_id[i] = ID_BASE[randomChoice(BASE)]; + } + return return_generate_id; +} + +/** string returned from baseconvert */ +static char return_baseconvert[ORDER_ID_LENGTH+1]; + +/** converts decimal number to BASE (base 65) + * @return string representing a number in base BASE + * @param[in] n integer + * @ingroup EDI_CORE + */ +const char *baseconvert(i64 n) { + if (n < 0) { + return_baseconvert[0] = 0; + } + else { + return_baseconvert[ORDER_ID_LENGTH] = 0; + u8 i = ORDER_ID_LENGTH-1; + while (1) { + i64 r = n % BASE; + return_baseconvert[i] = ID_BASE[r]; + i--; + n = n / BASE; + if (n == 0) + break; + } + rangeDown(j, i+1) { + return_baseconvert[j] = ID_BASE[0]; + } + } + return return_baseconvert; +} + +/** converts BASE number to decimal + * @return n integer + * @param[in] n string representing a number in base BASE + * @ingroup EDI_CORE + */ +i64 baseconvert_to_dec(const char *n) { + i64 r = 0; + i64 power = 1; + rangeDown(digit, lenG(n)) { + int i = 0; + range(a, BASE) { + if (ID_BASE_STRING[a] == getG(n, unusedV, digit)) { + break; + } + i++; + } + r += i * power; + power *= BASE; + } + return r; +} + +/** string returned from add_task_to_group_folder */ +static char return_add_task_to_group_folder[8192]; + +/** add task to group folder in database groups + * @param[in] tid task id + * @param[in] group task id + * @ingroup EDI_CORE + * Tasks are added at the top or bottom of the list. + */ +const char *add_task_to_group_folder(const char *tid, const char *group) { + // Create an entry in group + smallArrayt *tasks = NULL; + const char *order_id; + tasks = readDirG(rtSmallArrayt, generate_group_path(group)); + // Add +1 to last order_id to have the task last in the list + if (tasks and (lenG(tasks) > 0)) { + if ((lenG(tasks) == 1) or (eqG(add_top_or_bottom, "bottom"))) { + // add tasks in bottom + char *s = copyRngG(getG(tasks, rtChar, -1), 0, ORDER_ID_LENGTH); + order_id = baseconvert(baseconvert_to_dec(s)+1); + free(s); + } + else { + // add tasks on top + // temporary orderid # + char orderid_and_tid[ORDER_ID_LENGTH + ID_LENGTH + 1]; + range(i, ORDER_ID_LENGTH) { + orderid_and_tid[i] = '#'; + } + strcat(orderid_and_tid, tid); + i64 to_pos; + if (eqG(group, "root")) { + to_pos = 0; + } + else { + // add new tasks after group title + to_pos = 1; + } + + injectSG(tasks, to_pos, orderid_and_tid); + + // Move tasks + const char *path = generate_group_path(group); + enumerateSmallArray(tasks, T, n) { + castS(t, T); + if (n == to_pos) { + // set orderid on top + order_id = baseconvert(n); + } + else { + char src[8192]; + sprintf(src, "%s/%s", path, ssGet(t)); + char dst[8192]; + sprintf(dst, "%s/%s%s", path, baseconvert(n), ssGet(t)+ORDER_ID_LENGTH); + shRename(src, dst); + } + finishG(t); + } + } + } + else { + // start at 0 when group is empty + order_id = baseconvert(0); + } + + sprintf(return_add_task_to_group_folder, "%s/%s%s", generate_group_path(group), order_id, tid); + // remove double slash that can come up + char *tmp = return_add_task_to_group_folder; + iUniqSlash(tmp); + FILE *f = fopen(return_add_task_to_group_folder, "w"); + fclose(f); + + // return return_add_task_to_group_folder to easily add new tasks to group_directory_file_list and save time + return return_add_task_to_group_folder; +} + +/** creates a task and opens vi + * @param[in] group task id + * @ingroup EDI_CORE + */ +const char *create_task(const char *group) { + // Open text editor + // create task in tasks folder + //#:define create_task_part1 + const char *tid = generate_id(); + + // Save text in tasks + const char *task_path = generate_task_path(tid); + mkdirParentsG(task_path); + //#:end + // create an empty file (for windows and mac osx) + createTaskPath(dPath, "description.txt"); + FILE *f = fopen(dPath, "w"); + fclose(f); + if (hasG(editor, "echo TEST")) { + // use os.system to be able to run edi cli unittests + //char *c = catG(editor, dPath); + char *c = catS("echo TEST > ", dPath); + systemNFreeG(c); + } + else { + // use subprocess.call to wait until the editor is closed (Windows and Mac OSX) because the task is deleted when the description is empty + // create a list [command,param1,param2,task file] to allow options on command line to start the editor + smallStringt *ss = allocG(editor); + smallArrayt *editor_l = splitG(ss, "/"); + terminateG(ss); + // 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 + ss = getG(editor_l, rtSmallStringt, -1); + smallArrayt *options = splitG(ss, " "); + finishG(ss); + setNFreeG(editor_l, -1, getNDupG(options, rtBaset, 0)); + delG(options, 0, 1); + createAllocateSmallArray(command_line); + ss = joinG(editor_l, "/"); + terminateG(editor_l); + pushG(command_line, ss); + appendNSmashG(command_line, options); + rallocG(ss, dPath); + pushNFreeG(command_line, ss); + ss = joinG(command_line, " "); + terminateG(command_line); + systemNFreeG(ss); + } + if (not fileExists(dPath)) { + // file doesnt exist, abort task creation + rmAllG(task_path); + return "no new task, the description is empty"; + } + + // Save creation time + //#:define save_task_creation_time + time_t t = getModificationTimeG(dPath); + char *s = timeToS(t); + genTaskPath(dPath, "ctime.txt"); + writeFileG(s, dPath); + free(s); + //#:end + + // create status, active by default + //#:define create_task_part2 + // Create status + genTaskPath(dPath, "status"); + writeFileG(TASK_STATUS[TASK_STATUS_ACTIVE], dPath); + + const char *tid_in_groups_path = add_task_to_group_folder(tid, group); + + if (lenG(group_directory_file_list)) { + pushG(group_directory_file_list, tid_in_groups_path); + } + + // autolink + if (autolink) { + forEachSmallArray(autolink, G) { + castS(g, G); + if (is_this_task_a_group(ssGet(g))) { + // link to groups existing in current database + add_task_reference_to_a_group(tid, g); + } + finishG(g); + } + } + //#:end + sprintf(dPath, "created %s in group %s", tid, group); + edi_log(dPath); + return tid; +} + +/** create task and copy text file + * @param[in] group task id + * @param[in] text_file filename of text file to copy + * @ingroup EDI_CORE + */ +const char *add_task(const char *group, const char *text_file) { + //#:create_task_part1 + const char *tid = generate_id(); + + // Save text in tasks + const char *task_path = generate_task_path(tid); + mkdirParentsG(task_path); + createTaskPath(dPath, "description.txt"); + copy(text_file, dPath); + //#:save_task_creation_time + time_t t = getModificationTimeG(dPath); + char *s = timeToS(t); + genTaskPath(dPath, "ctime.txt"); + writeFileG(s, dPath); + free(s); + //#:create_task_part2 + // Create status + genTaskPath(dPath, "status"); + writeFileG(TASK_STATUS[TASK_STATUS_ACTIVE], dPath); + + const char *tid_in_groups_path = add_task_to_group_folder(tid, group); + + if (lenG(group_directory_file_list)) { + pushG(group_directory_file_list, tid_in_groups_path); + } + + // autolink + if (autolink) { + forEachSmallArray(autolink, G) { + castS(g, G); + if (is_this_task_a_group(ssGet(g))) { + // link to groups existing in current database + add_task_reference_to_a_group(tid, g); + } + finishG(g); + } + } + sprintf(dPath, "created %s in group %s", tid, group); + edi_log(dPath); + return tid; +} + +/** create task with description text + * @param[in] group task id + * @param[in] text string + * @ingroup EDI_CORE + */ +const char *add_text(const char *group, smallStringt *text) { + //#:create_task_part1 + const char *tid = generate_id(); + + // Save text in tasks + const char *task_path = generate_task_path(tid); + mkdirParentsG(task_path); + createTaskPath(dPath, "description.txt"); + writeFileG(text, dPath); + //#:save_task_creation_time + time_t t = getModificationTimeG(dPath); + char *s = timeToS(t); + genTaskPath(dPath, "ctime.txt"); + writeFileG(s, dPath); + free(s); + //#:create_task_part2 + // Create status + genTaskPath(dPath, "status"); + writeFileG(TASK_STATUS[TASK_STATUS_ACTIVE], dPath); + + const char *tid_in_groups_path = add_task_to_group_folder(tid, group); + + if (lenG(group_directory_file_list)) { + pushG(group_directory_file_list, tid_in_groups_path); + } + + // autolink + if (autolink) { + forEachSmallArray(autolink, G) { + castS(g, G); + if (is_this_task_a_group(ssGet(g))) { + // link to groups existing in current database + add_task_reference_to_a_group(tid, g); + } + finishG(g); + } + } + sprintf(dPath, "created %s in group %s", tid, group); + edi_log(dPath); + return tid; +} + +/** create task and copy text file, filename is task title + * @param[in] group task id + * @param[in] text_file filename of text file to copy + * @ingroup EDI_CORE + */ +const char *add_task_and_filename(const char *group, const char *text_file) { + const char *tid = add_task(group,text_file); + + // Add file name on first line + createTaskPath(dPath, "description.txt"); + createAllocateSmallArray(f); + readFileG(f, dPath); + char *s = basename(text_file); + prependG(f, s); + writeFileG(f, dPath); + terminateG(f); + return tid; +} + +/** string returned from add_many_tasks_array */ +static char return_add_many_tasks_array[100]; + +/** create many tasks from a text array + * @param[in] group task id + * @param[in] text_array text array to copy + * @ingroup EDI_CORE + * Used directly in Easydoneit Desktop + */ +const char *add_many_tasks_array(const char *group, smallArrayt *text_array) { + return_add_many_tasks_array[0] = 0; + + // Copy a task to text string + // Remove '#' from first line, first character position + // Add task to database + createAllocateSmallString(text); + i64 number_of_tasks = 0; + enum {START, WRITING_TASK}; + i8 status = START; + forEachSmallArray(text_array, L) { + castS(l, L); + if (eqG(l, "---")) { + if (status != START) { + // add task to database + add_text(group,text); + number_of_tasks++; + } + emptyG(text); + status = START; + } + else { + if ((status == START) and (ssGet(l)[0] == '#')) { + delG(l, 0, 1); + } + appendNSmashG(text, dupG(l)); + status = WRITING_TASK; + } + finishG(l); + } + + if (status == WRITING_TASK) { + // add task to database + add_text(group,text); + number_of_tasks++; + } + terminateG(text); + + sprintf(return_add_many_tasks_array, "created %ld tasks in group %s", number_of_tasks, group); + edi_log(return_add_many_tasks_array); + return return_add_many_tasks_array; +} + +/** create many tasks from a text file + * @param[in] group task id + * @param[in] text_file filename of text file to copy + * @ingroup EDI_CORE + */ +const char *add_many_tasks(const char *group, const char *text_file) { + + // load file and call add_many_tasks_array + createAllocateSmallArray(text_array); + readFileG(text_array, text_file); + const char *r = add_many_tasks_array(group, text_array); + terminateG(text_array); + return r; +} + +/** string returned from add_many_one_line_tasks_array */ +static char return_add_many_one_line_tasks_array[100]; + +/** create many one line tasks from a text array + * @param[in] group task id + * @param[in] text_array text array to copy + * @ingroup EDI_CORE + * Used directly in Easydoneit Desktop + */ +const char *add_many_one_line_tasks_array(const char *group, smallArrayt *text_array) { + return_add_many_one_line_tasks_array[0] = 0; + + i64 number_of_tasks = 0; + forEachSmallArray(text_array, L) { + castS(l, L); + trimG(l); + add_text(group,l); + number_of_tasks++; + finishG(l); + } + + sprintf(return_add_many_one_line_tasks_array, "created %ld tasks in group %s", number_of_tasks,group); + edi_log(return_add_many_one_line_tasks_array); + return return_add_many_one_line_tasks_array; +} + +/** create many one line tasks from a text file + * @param[in] group task id + * @param[in] text_file filename of text file to copy + * @ingroup EDI_CORE + */ +const char *add_many_one_line_tasks(const char *group, const char *text_file) { + + createAllocateSmallArray(text_array); + readFileG(text_array, text_file); + const char *r = add_many_one_line_tasks_array(group, text_array); + terminateG(text_array); + return r; +} + +/** string returned from add_many_groups_from_text_array */ +static char return_add_many_groups_from_text_array[100]; + +/** create group from text array + * @param[in] group task id + * @param[in] text_array with group structure + * @ingroup EDI_CORE + * Used directly in Easydoneit Desktop + */ +const char *add_many_groups_from_text_array(const char *group, smallArrayt *text_array) { + return_add_many_groups_from_text_array[0] = 0; + + i64 number_of_tasks = 0; + createAllocateSmallArray(group_stack); + pushG(group_stack, group); + forEachSmallArray(text_array, L) { + castS(l, L); + // count space indents + i64 indent = 0; + char *s = ssGet(l); + range(i, lenG(l)) { + if (s[i] != ' ') { + break; + } + indent++; + } + if (indent < lenG(group_stack)-1) { + // remove groups from stack if same level or higher levels + delG(group_stack, -(lenG(group_stack)-1 - indent), 0); + } + trimG(l); + const char *tid = add_text(getG(group_stack, rtChar,-1), l); + create_group(tid); + pushG(group_stack, tid); + number_of_tasks++; + finishG(l); + } + + sprintf(return_add_many_groups_from_text_array, "created %ld groups in group %s", number_of_tasks, group); + edi_log(return_add_many_groups_from_text_array); + return return_add_many_groups_from_text_array; +} + +/* create group from text file + * @param[in] group task id + * @param[in] text_file with group structure + * @ingroup EDI_CORE + */ +const char *add_many_groups_from_text(const char *group, const char *text_file) { + + // load file and call add_many_groups_from_text_array + createAllocateSmallArray(text_array); + readFileG(text_array, text_file); + const char *r = add_many_groups_from_text_array(group, text_array); + terminateG(text_array); + + return r; +} + +/** string returned from export_task_to_a_file */ +static char return_export_task_to_a_file[8192]; + +/** copy description to path using first line of description as filename + * @return path and filname + * @param[in] tid task id + * @param[in] path destination directory for task description + * @ingroup EDI_CORE + */ +const char *export_task_to_a_file(const char *tid, const char *path) { + createTaskPath(dPath, "description.txt"); + createAllocateSmallArray(des); + readFileG(des, dPath); + smallStringt *fn = getNDupG(des, rtSmallStringt, 0); + trimG(fn); + delG(des, 0, 1); + sprintf(return_export_task_to_a_file, "%s/%s", path, ssGet(fn)); + // remove eventual double // + uniqSlash((char *)&return_export_task_to_a_file); + + writeFileG(des, return_export_task_to_a_file); + return return_export_task_to_a_file; +} + +/** print description of task tid + * @return list of strings + * @param[in] tid task id + * @param[in] titleFunc Function that changes first description line + * @ingroup EDI_CORE + * In GUI, titleFunc (passThroughTitle function) keeps original title line. + */ +// python call: def display_task tid, titleFunc=generate_task_string_with_tid +smallArrayt *display_task(const char *tid, smallStringt *titleFunc(const char *, smallStringt *)) { + createTaskPath(task_path, "description.txt"); + createAllocateSmallArray(description); + if (isBlankG(tid)) { + // tid is invalid, return empty description + pushG(description, ""); + return description; + } + readFileG(description, task_path); + + // print tid, status and first line + smallStringt *s = getG(description, rtSmallStringt, 0); + if (s) { + setNFreeG(description, 0, titleFunc(tid, s)); + } + else { + // empty description, push empty line + pushG(description, ""); + } + finishG(s); + return description; +} + + +/** string returned frm find_group_containing_task */ +static char return_find_group_containing_task[64]; + +/** find group containing task tid in groups folder + * @return tid + * @param[in] tid task id + * @ingroup EDI_CORE + */ +const char *find_group_containing_task(const char *tid) { + if (eqG(tid, "root")) { + // root has not parent group, return root + return tid; + } + return_find_group_containing_task[0] = 0; + //#:define walk_groups + // reuse previously created group list, to save time + if (not lenG(group_directory_file_list)) { + terminateG(group_directory_file_list); +; group_directory_file_list = walkDirG(rtSmallArrayt, data_location_groups); + } + smallArrayt *groups_and_tasks = group_directory_file_list; + forEachSmallArray(groups_and_tasks, GT) { + castS(t, GT); + smallArrayt *ts = splitG(t, "/"); + if (endsWithG(t, tid) and not eqG(tid, getG(ts, rtChar, -2))) { + strcpy(return_find_group_containing_task, getG(ts, rtChar, -2)); + } + terminateG(ts); + finishG(GT); + } + //#:end + if (return_find_group_containing_task[0] == 0) { + printf("\n\nEDI_ERROR: Database inconsistent - run: find %s|grep %s and remove reference.\n", data_location, tid); + strcpy(return_find_group_containing_task, "error"); + } + return return_find_group_containing_task; +} + +/** find group containing task tid in groups folder and print + * @return list of strings + * @param[in] tid task id + * @ingroup EDI_CORE + * Shows path in tree and title for each group in path + */ +smallArrayt *show_group_for_task(const char *tid) { + createAllocateSmallArray(r); + + // set group string to be displayed on first line + char *group_s = " "; + if (is_this_task_a_group(tid)) { + group_s = "GROUP"; + } + if (is_linked(tid)) { + group_s = " LINK"; + } + + //#:walk_groups + // reuse previously created group list, to save time + if (not lenG(group_directory_file_list)) { + terminateG(group_directory_file_list); + group_directory_file_list = walkDirG(rtSmallArrayt, data_location_groups); + } + smallArrayt *groups_and_tasks = group_directory_file_list; + createAllocateSmallString(treePath); + forEachSmallArray(groups_and_tasks, GT) { + castS(t, GT); + smallArrayt *ts = splitG(t, "/"); + if (endsWithG(t, tid) and not eqG(tid, getG(ts, rtChar, -2))) { + char *group = return_find_group_containing_task; + strcpy(return_find_group_containing_task, getG(ts, rtChar, -2)); + const char *tree_path = find_group_in_tree(group); + + // p is data_location folder/tree + smallStringt *s = allocG(data_location_tree); + smallArrayt *p_l = splitG(s, "/"); + terminateG(s); + sliceG(p_l, -2, 0); + smallStringt *p = joinG(p_l, "/"); + terminateG(p_l); + + // if empty then command is run in tree root + setFromG(treePath, tree_path); + p_l = splitG(treePath, ssGet(p)); + if (lenG(getG(p_l, rtChar, -1))) { + // print path of tids: tid/tid... + s = getNDupG(p_l, rtSmallStringt, -1); + // remove / + delG(s, 0, 1); + pushG(r, s); + smallArrayt *g_l = splitG(s, "/"); + finishG(s); + createAllocateSmallArray(group_titles_in_path); + forEachSmallArray(g_l, G) { + castS(g, G); + pushNFreeG(group_titles_in_path, get_task_title(ssGet(g))); + finishG(g); + } + terminateG(g_l) + // print title/title... + pushNFreeG(r, joinG(group_titles_in_path, "/")); + terminateG(group_titles_in_path); + } + terminateManyG(p, p_l); + p = get_task_title(group); + pushNFreeG(r, generate_group_string_with_tid(group, p)); + terminateG(p); + pushG(r, ""); + } + terminateG(ts); + finishG(GT); + } + terminateG(treePath); + + // Print media and attachments + // media type + smallArrayt *m = get_media(tid); + char *media = getG(m, rtChar, 0); + // attachment list + smallArrayt *attachments = get_attachments(tid); + + // Print task, colors, group list + smallArrayt *color = get_forground_color(tid); + char *fc = toStringG(color); + terminateG(color); + color = get_background_color(tid); + char *bc = toStringG(color); + // Figure out creation time and modification times for description and status + smallArrayt *tctime = get_creation_date(tid); + char *task_ctime = getG(tctime, rtChar, 0); + + createTaskPath(task_path, "description.txt"); + char *description_mtime = timeToS(getModificationTimeG(task_path)); + genTaskPath(task_path, "status"); + char *status_mtime = timeToS(getModificationTimeG(task_path)); + smallStringt *tt = get_task_title(tid); + prependG(tt, " "); + prependG(tt, group_s); + smallStringt *s = generate_task_string_with_tid(tid, tt); + createAllocateSmallArray(info); + pushG (info, "Tasks:"); + pushG (info, ssGet(s)); + pushG (info, ""); + pushG (info, ""); + terminateManyG(tt, s); + pushNFreeG (info, appendG("Media type: ", media)); + pushG (info, ""); + pushG (info, "Attachments:"); + appendNSmashG (info, attachments); + pushG (info, ""); + pushG (info, ""); + pushNFreeG (info, formatS("foreground color: %s", fc)); + pushNFreeG (info, formatS("background color: %s", bc)); + pushNFreeG (info, formatS("Creation time: %s", task_ctime)); + pushNFreeG (info, formatS("Last description change: %s", description_mtime)); + pushNFreeG (info, formatS("Last status change: %s", status_mtime)); + pushG (info, ""); + pushG (info, "Group list"); + appendNSmashG (info, r); + r = info; + terminateManyG(m, color, tctime); + freeManyS(fc, bc, description_mtime, status_mtime); + return r; +} + +/** string returned from find_group_in_tree */ +static char return_find_group_in_tree[8192]; + +/** find group in tree folder + * @return path_in_tree + * @param[in] tid task id + * @ingroup EDI_CORE + */ +const char *find_group_in_tree(const char *group) { + char *path_in_tree; + if (eqG(group, "root")) { + path_in_tree = data_location_tree; + } + else { + path_in_tree = return_find_group_in_tree; + path_in_tree[0] = 0; + + // list all group paths in tree + /* if platform.system() = 'Windows' */ + /* folders = [] */ + /* folders append data_location_tree */ + /* for dir, subdirs, files in os.walk(data_location_tree) */ + /* for sd in subdirs */ + /* folders append dir+os.sep + sd */ + /* else */ + smallArrayt *folders = walkDirAllG(rtSmallArrayt, data_location_tree); + enumerateSmallArray(folders, F, i) { + castS(f, F); + trimG(f); + setNFreePG(folders, i, f); + } + smallArrayt *groups = folders; + // find the group in group paths + forEachSmallArray(groups, G) { + castS(g, G); + char *s = basename(ssGet(g)); + if (eqG(s, group)) + strcpy(path_in_tree, ssGet(g)); + finishG(g); + } + terminateG(folders); + } + return path_in_tree; +} + +/** Determines if a task has multiple references + * @return integer 0 or 1 + * @param[in] tid task id + * @ingroup EDI_CORE + */ +bool is_linked(const char *tid) { + bool status = false; + createTaskPath(task_linked_groups_path, "groups/"); + // check if tid/groups exists + if (fileExists(task_linked_groups_path)) { + smallArrayt *groups = readDirG(rtSmallArrayt, task_linked_groups_path); + // check if task is linked to more than 1 group + if (lenG(groups) > 1) { + status = true; + } + terminateG(groups); + } + return status; +} + +/** convert task to group + * @return list of stings when there is an error + * @param[in] tid task id + * @ingroup EDI_CORE + */ +smallArrayt *create_group(const char *tid) { + createAllocateSmallArray(r); + // convert task to group only when task is not linked + if (is_linked(tid)) { + pushG(r, "Converting linked task to group removes links. The task groups are:"); + appendNSmashG(r, show_group_for_task(tid)); + + // delete tid/groups because groups are not linked + createTaskPath(task_linked_groups_path, "groups/"); + smallArrayt *groups = readDirG(rtSmallArrayt, task_linked_groups_path); + // remove all links except for the first group + forEachSmallArray(groups, G) { + castS(g, G); + delete_linked_task(ssGet(g),tid); + finishG(g); + } + if (fileExistsG(task_linked_groups_path)) { + rmAll(task_linked_groups_path); + } + pushNFreeG(r, formatS("Created group %s in %s", tid, getG(groups, rtChar,0))); + terminateG(groups); + } + // create new group in groups folder + char p[8192]; + sprintf(p, "%s/%s", data_location_groups, tid); + mkdirParentsG(p); + // First task in group is group title task + const char *order_id = baseconvert(0); + sprintf(p, "%s/%s%s", generate_group_path(tid), order_id, tid); + FILE *f = fopen(p, "w"); + fclose(f); + + // Add group in tree + // update group list + emptyG(group_directory_file_list); + const char *group = find_group_containing_task(tid); + if (eqG(group, "root")) { + sprintf(p, "%s/%s", data_location_tree, tid); + mkdirParentsG(p); + // git support + sprintf(p, "%s/%s/.keepDir", data_location_tree, tid); + f = fopen(p, "w"); + fclose(f); + } + else { + const char *s = find_group_in_tree(group); + sprintf(p, "%s/%s", s, tid); + mkdirParentsG(p); + // git support + sprintf(p, "%s/%s/.keepDir", s, tid); + f = fopen(p, "w"); + fclose(f); + } + + // Change status active to void by default + // To avoid filtering groups + if (eqG(get_status(tid), TASK_STATUS[TASK_STATUS_ACTIVE])) { + // set status to void + set_status(tid,TASK_STATUS_VOID); + } + sprintf(p, "created group %s", tid); + edi_log(p); + return r; +} + +/** string returned from convert_group_to_task */ +static char return_convert_group_to_task[1]; + +/** convert group to task + * @param[in] group group to convert to task + * @ingroup EDI_CORE + */ +const char *convert_group_to_task(const char *group) { + char *r = return_convert_group_to_task; + r[0] = 0; + + // list all tasks (data_location_groups/GROUP/'ORDER_ID''TASK_ID') in groups folder + smallArrayt *groups_and_tasks = walkDirG(rtSmallArrayt, data_location_groups); + + char *convert_group_status = "delete"; + char groupslash[ORDER_ID_LENGTH + ID_LENGTH + 1]; + sprintf(groupslash, "%s/", group); + forEachSmallArray(groups_and_tasks, T2) { + castS(t2, T2); + // search a task in group that is not tid + char *t2task = basename(ssGet(t2)); + if (hasG(t2, groupslash) and (not eqG(group, t2task))) + convert_group_status = "There is another task in the group, keeping group."; + r = convert_group_status; + // just need to find one task + break; + finishG(t2); + } + terminateG(groups_and_tasks); + if (eqG(convert_group_status, "delete") and (not hasG(group, "root"))) { + // Delete group title and group folder + rmAllG(generate_group_path(group)); + // Delete group in tree + const char *treepath = find_group_in_tree(group); + if (not isEmptyG(treepath)) { + rmAll(treepath); + } + // change state from void to active + set_status(group,TASK_STATUS_ACTIVE); + char log[100]; + sprintf(log, "converted group %s to task", group); + edi_log(log); + } + return r; +} + +/** string returned from delete_task */ +static char return_delete_task[ORDER_ID_LENGTH + ID_LENGTH + 1]; + +/** delete task tid in all groups + * @return group id + * @param[in] tid task id + * @ingroup EDI_CORE + */ +const char *delete_task(const char *tid) { + // Delete task in tasks + rmAll(generate_task_path(tid)); + + // Delete task reference in groups + // list all tasks (data_location_groups/GROUP/'ORDER_ID''TASK_ID') in groups folder + smallArrayt *groups_and_tasks = walkDirG(rtSmallArrayt, data_location_groups); + + // Identify if task is a group + bool is_a_group = is_this_task_a_group(tid); + + // Return group task, it changes when first task is deleted + char *group_id = return_delete_task; + group_id[0] = 0; + // list groups to reorder at the end + createAllocateSmallArray(reorder_groups); + forEachSmallArray(groups_and_tasks, T) { + castS(t, T); + // Delete reference in upper group, not the title task of the group + smallArrayt *ta = splitG(t, "/"); + char thisGroup[8192]; + strcpy(thisGroup, getG(ta, rtChar, -2)); + terminateG(ta); + if ((endsWithG(t, tid)) and (not eqG(tid, thisGroup))) { + // group is the group for task tid + strcpy(group_id, thisGroup); + // Delete task reference in group + rmAllG(t); + pushG(reorder_groups, group_id); + if (is_a_group) { + // First task becomes a group, add a reference in group group + // list tasks in order in tid group + smallArrayt *group_tasks = readDirG(rtSmallArrayt, generate_group_path(tid)); + // Delete emtpy group or first task in group becomes the group title. + if (lenG(group_tasks) == 1) { + rmAllG(generate_group_path(tid)); + // Delete group in tree + // when a group is deleted, subgroups are automatically deleted in the tree + const char *s = find_group_in_tree(tid); + if (not isEmptyG(s)) { + rmAllG(s); + } + } + else { + // Remove order_id, keep task id only + char *first_task = getG(group_tasks, rtChar,1) + ORDER_ID_LENGTH; + strcpy(group_id, first_task); + // Create an entry in group at the same position as tid had + ta = splitG(t, "/"); + char *order_id = copyRngG(getG(ta, rtChar, -1), 0, ORDER_ID_LENGTH); + terminateG(ta); + char groupPath[8192]; + sprintf(groupPath, "%s/%s%s", generate_group_path(group_id), order_id, first_task); + free(order_id); + FILE *f = fopen(groupPath, "w"); + fclose(f); + + // Delete group task of group of more then 2 tasks, first task becomes a group + // delete orderidtaskid + sprintf(groupPath, "%s/%s", generate_group_path(tid), getG(group_tasks, rtChar, 0)); + rmAllG(groupPath); + const char *path = generate_group_path(tid); + char src[8192]; + sprintf(src, "%s/%s", path, getG(group_tasks, rtChar, 1)); + char dst[8192]; + sprintf(dst, "%s/%s%s", path, baseconvert(0), first_task); + shRename(src, dst); + // reorder tasks to remove gap between group title task and first task + smallArrayt *tasks = readDirG(rtSmallArrayt, path); + enumerateSmallArray(tasks, T, n) { + castS(t, T); + char src[8192]; + sprintf(src, "%s/%s", path, ssGet(t)); + char dst[8192]; + sprintf(dst, "%s/%s%s", path, baseconvert(n), ssGet(t)+ORDER_ID_LENGTH); + shRename(src, dst); + finishG(t); + } + terminateG(tasks); + // rename group in groups folder + shRename(path, generate_group_path(first_task)); + // rename group in tree folder + const char *group_tree_path = find_group_in_tree(tid); + smallStringt *s = allocG(group_tree_path); + smallArrayt *group_tree_path_l = splitG(s, "/"); + terminateG(s); + setG(group_tree_path_l, -1, first_task); + smallStringt *new_group_tree_path = joinG(group_tree_path_l, "/"); + shRename(group_tree_path, ssGet(new_group_tree_path)); + terminateManyG(new_group_tree_path, group_tree_path_l); + } + terminateG(group_tasks); + } + } + finishG(t); + } + terminateG(groups_and_tasks); + + // reorder tasks to remove gaps in reorder_groups + forEachSmallArray(reorder_groups, T) { + castS(tidg, T); + const char *path = generate_group_path(ssGet(tidg)); + smallArrayt *tasks = readDirG(rtSmallArrayt, path); + enumerateSmallArray(tasks, TD, n) { + castS(t, TD); + char src[8192]; + sprintf(src, "%s/%s", path, ssGet(t)); + char dst[8192]; + sprintf(dst, "%s/%s%s", path, baseconvert(n), ssGet(t)+ORDER_ID_LENGTH); + shRename(src, dst); + finishG(t); + } + terminateG(tasks); + finishG(tidg); + } + + // Return group task + smallStringt *s = joinG(reorder_groups, " "); + char *log = formatS("deleted %s in %s", tid, ssGet(s)); + edi_log(log); + free(log); + terminateManyG(reorder_groups, s); + return group_id; +} + +/** string returned from delete_linked_task */ +static char return_delete_linked_task[ORDER_ID_LENGTH + ID_LENGTH + 1]; + +/** Delete task only if it is linked in one group + * @return group id + * @param[in] group task id + * @param[in] tid task id + * @ingroup EDI_CORE + */ +const char *delete_linked_task(const char *group, const char *tid) { + char *group_id = return_delete_linked_task; + strcpy(group_id, group); + + if (not is_linked(tid)) { + group_id = (char *) delete_task(tid); + } + else { + // Delete task reference in group + const char *path = generate_group_path(group); + char p[8192]; + + // find task in group: ORDER_IDTASK_ID + smallArrayt *tasks = readDirG(rtSmallArrayt, path); + forEachSmallArray(tasks, T) { + castS(t, T); + if (eqG(ssGet(t)+ORDER_ID_LENGTH, tid)) { + sprintf(p, "%s%s", path, ssGet(t)); + rmAllG(p); + } + finishG(t); + } + terminateG(tasks); + + // delete group in tid/groups + sprintf(p, "%s/groups/%s", generate_task_path(tid), group); + rmAllG(p); + + // reorder tasks to remove gaps in group + tasks = readDirG(rtSmallArrayt, path); + enumerateSmallArray(tasks, T, n) { + castS(t, T); + char src[8192]; + sprintf(src, "%s/%s", path, ssGet(t)); + char dst[8192]; + sprintf(dst, "%s/%s%s", path, baseconvert(n), ssGet(t)+ORDER_ID_LENGTH); + shRename(src, dst); + finishG(t); + } + terminateG(tasks); + + sprintf(p, "deleted %s in group %s", tid, group); + edi_log(p); + } + + return group_id; +} + +/** delete group tid + * @return group id + * @param[in] tid task id + * @ingroup EDI_CORE + */ +const char *delete_group(const char *tid) { + // Delete tasks in group + smallArrayt *group_tasks = readDirG(rtSmallArrayt, generate_group_path(tid)); + if ((lenG(group_tasks) == 0) and eqG(tid, "root")) { + // the database is already empty + return tid; + } + if (not eqG(tid, "root")) { + // root group doesnt have a title + // Remove group title from the loop to delete tasks only and then group + delG(group_tasks, 0, 1); + } + + // Delete tasks and groups recursively + forEachSmallArray(group_tasks, T) { + castS(t, T); + // Remove order_id, keep task id only + char *oid = ssGet(t)+ORDER_ID_LENGTH; + if (not is_this_task_a_group(oid)) { + // delete tasks that are linked only once + delete_linked_task(tid, oid); + } + else { + delete_group(oid); + } + finishG(t); + } + terminateG(group_tasks); + + if (not eqG(tid, "root")) { + // never delete root group + return delete_task(tid); + } + else { + // return root and keep root task + return tid; + } +} + +/** edit task with vi + * @param[in] tid task id + * @ingroup EDI_CORE + */ +void edit_task(const char *tid) { + createTaskPath(dPath, "description.txt"); + char c[24576]; + sprintf(c, "%s %s", editor, dPath); + system(c); + sprintf(c, "edited %s", tid); + edi_log(c); +} + +/** move task from group at at_pos to to_pos and reorder + * @return list of stings when there is an error + * @param[in] group task id + * @param[in] at_pos selected position + * @param[in] to_pos insert position + * @ingroup EDI_CORE + */ +smallArrayt *change_task_order(const char *group, i64 at_pos, i64 to_pos) { + // List tasks in group + createAllocateSmallArray(r); + char path[8192]; + strcpy(path, generate_group_path(group)); + smallArrayt *tasks = readDirG(rtSmallArrayt, path); + + // Verify position + // Get task + char *orderid_and_tid = getG(tasks, rtChar, at_pos); + if (not orderid_and_tid) { + pushNFreeG(r, formatS("%ld is an invalid position.", at_pos)); + goto end; + } + if (to_pos == 0) { + char *tid = orderid_and_tid+ORDER_ID_LENGTH; + // do not move linked tasks to position 0 + if (is_linked(tid)) { + pushG(r, "Converting linked task to group removes links. The task groups are:"); + appendNSmashG(r, show_group_for_task(tid)); + + // delete tid/groups because groups are not linked + createTaskPath(task_linked_groups_path, "groups/"); + smallArrayt *groups = readDirG(rtSmallArrayt, task_linked_groups_path); + // remove all links except for the first group + forEachSmallArray(groups, G) { + castS(g, G); + if (not eqG(g, group)) { + delete_linked_task(ssGet(g),tid); + } + finishG(g); + } + terminateG(groups); + if (fileExists(task_linked_groups_path)) { + rmAll(task_linked_groups_path); + } + const char *parent_group = find_group_containing_task(group); + pushNFreeG(r, formatS("Created group %s in %s",tid,parent_group)); + } + // do not move groups to position 0 + if (is_this_task_a_group(tid)) { + pushG(r, "Having a group in group title is not supported."); + goto end; + } + } + // Insert task at to_pos + char *to_pos_tid = NULL; + if (to_pos > at_pos) { + // +1 because at_pos reference will be deleted and will shift to_pos reference + to_pos += 1; + injectSG(tasks, to_pos, orderid_and_tid); + // Delete task at at_pos + delG(tasks, at_pos, at_pos+1); + } + else { + // rename group title, when to_pos in 0 + if ((to_pos == 0) and (not eqG(group, "root"))) { + to_pos_tid = getG(tasks, rtChar, to_pos)+ORDER_ID_LENGTH; + } + + injectSG(tasks, to_pos, orderid_and_tid); + // Delete task at at_pos+1 because the new position is before at_pos + // at_pos was shifted when to_pos reference was added above + delG(tasks, at_pos+1, at_pos+2); + } + + //:define reorder_tasks + // Move tasks + enumerateSmallArray(tasks, T, n) { + castS(t, T); + char src[8192]; + sprintf(src, "%s/%s", path, ssGet(t)); + char dst[8192]; + sprintf(dst, "%s/%s%s", path, baseconvert(n), ssGet(t)+ORDER_ID_LENGTH); + shRename(src, dst); + finishG(t); + } + //:end + + // rename group title, when to_pos in 0 + if ((to_pos == 0) and (not eqG(group, "root"))) { + // rename group in parent group + // group is tid for parent group and to_pos_tid is equal to group input parameter because to_pos is 0 + const char *groupF = find_group_containing_task(group); + // to_pos_orderid_and_tid is group in parent group + smallStringt *to_pos_orderid_and_tid; + smallArrayt *parent_group_tasks = readDirG(rtSmallArrayt, generate_group_path(groupF)); + forEachSmallArray(parent_group_tasks, T) { + castS(t, T); + if (eqG(ssGet(t)+ORDER_ID_LENGTH, to_pos_tid)) { + to_pos_orderid_and_tid = dupG(t); + } + finishG(t); + } + terminateG(parent_group_tasks); + // remove order_id, keep task id only for new group + char *group_id = orderid_and_tid+ORDER_ID_LENGTH; + // create an entry in parent group at the same position as to_pos tid had + smallStringt *order_id = copyRngG(to_pos_orderid_and_tid, 0, ORDER_ID_LENGTH); + char p[8192]; + sprintf(p, "%s/%s%s", generate_group_path(groupF), order_id,group_id); + FILE *f = fopen(p, "w"); + fclose(f); + + // delete group task of group + // delete orderidtaskid + sprintf(p, "%s/%s", generate_group_path(groupF), ssGet(to_pos_orderid_and_tid)); + rmAllG(p); + + // rename group in groups folder + strcpy(p, generate_group_path(to_pos_tid)); + shRename(p, generate_group_path(group_id)); + // rename group in tree folder + const char *group_tree_path = find_group_in_tree(to_pos_tid); + char **group_tree_path_l = splitG(group_tree_path, "/"); + setG(group_tree_path_l, -1, group_id); + char *new_group_tree_path = joinG(group_tree_path_l, "/"); + freeG(group_tree_path_l); + shRename(group_tree_path, new_group_tree_path); + free(new_group_tree_path); + + // rename group in linked task and former linked task with a 'groups' folder in tasks database folder + smallArrayt *group_tasks = readDirG(rtSmallArrayt, generate_group_path(group_id)); + forEachSmallArray(group_tasks, G) { + castS(group_task, G); + char tPath[8192]; + sprintf(tPath, "%s/groups", generate_task_path(ssGet(group_task)+ORDER_ID_LENGTH)); + if (fileExists(tPath)) { + // search for to_pos_tid (group input parameter) in task path groups folder + smallArrayt *link_groups = readDirG(rtSmallArrayt, tPath); + forEachSmallArray(link_groups, LG) { + castS(lg, LG); + if (eqG(to_pos_tid, lg)) { + // rename group to new group id since the title changed + char src[8192]; + sprintf(src, "%s/%s", tPath, to_pos_tid); + char dst[8192]; + sprintf(dst, "%s/%s", tPath, group_id); + shRename(src, dst); + } + finishG(lg); + } + terminateG(link_groups); + } + finishG(G); + } + terminateG(group_tasks); + } + + char log[2048]; + sprintf(log, "changed task order %s to %s in group %s", at_pos, to_pos, group); + edi_log(log); +end: + terminateG(tasks); + return r; +} + +/** move selected tasks in Easydoneit desktop from group to to_pos and reorder + * @return list of stings when there is an error + * @param[in] group task id + * @param[in] selected_tasks selected tasks in GUI list (array of ints) + * @param[in] to_pos insert position + * @ingroup EDI_CORE + */ +void change_task_order_desktop(const char *group,smallArrayt *selected_tasks, i64 to_pos) { + // List tasks in group + const char *path = generate_group_path(group); + smallArrayt *tasks = readDirG(rtSmallArrayt, path); + + createAllocateSmallArray(orderid_and_tids); + forEachSmallArray(selected_tasks, T) { + cast(smallIntt *, t, T); + pushG(orderid_and_tids, getG(tasks, rtChar, getG(t, rtI64, unusedV))); + finishG(t); + } + sortG(orderid_and_tids); + + smallArrayt *task1 = copyRngG(tasks, 0, to_pos); + smallArrayt *task2 = copyRngG(tasks, to_pos, 0); + + // remove selected tasks from group list + createAllocateSmallArray(task1m); + forEachSmallArray(task1, I) { + castS(i, I); + if (not binarySearchG(orderid_and_tids, ssGet(i))) { + pushG(task1m, i); + } + finishG(i); + } + createAllocateSmallArray(task2m); + forEachSmallArray(task2, I) { + castS(i, I); + if (not binarySearchG(orderid_and_tids, ssGet(i))) { + pushG(task2m, i); + } + finishG(i); + } + terminateManyG(task1, task2); + + emptyG(tasks); + catG(tasks, task1m, orderid_and_tids, task2m); + smashManyO(task1m, orderid_and_tids, task2m); + + //:reorder_tasks + // Move tasks + {enumerateSmallArray(tasks, T, n) { + castS(t, T); + char src[8192]; + sprintf(src, "%s/%s", path, ssGet(t)); + char dst[8192]; + sprintf(dst, "%s/%s%s", path, baseconvert(n), ssGet(t)+ORDER_ID_LENGTH); + shRename(src, dst); + finishG(t); + }} + terminateG(tasks); + + {enumerateSmallArray(orderid_and_tids, T, n) { + castS(t, T); + sliceG(t, ORDER_ID_LENGTH, 0); + setPG(orderid_and_tids, n, t); + finishG(t); + }} + smallStringt *log = joinG(orderid_and_tids, " "); + terminateG(orderid_and_tids); + char *s = formatS("changed task order (%s) to %ld in group %s", ssGet(log), to_pos, group); + terminateG(log); + edi_log(s); + free(s); +} + +/** string returned from move_task_to_a_group */ +static char return_move_task_to_a_group[2048]; + +/** move task to group + * @return tid + * @param[in] tgroup task id, select a task in this group + * @param[in] tid task id + * @param[in] group task id, destination group + * @ingroup EDI_CORE + */ +const char *move_task_to_a_group(const char *tgroup, const char *tid, const char *group) { + + // find task tid in tgroup and remove task + char path[8192]; + strcpy(path, generate_group_path(tgroup)); + smallArrayt *tasks = readDirG(rtSmallArrayt, path); + char task_in_group[ORDER_ID_LENGTH + ID_LENGTH +1]; + task_in_group[0] = 0; + enumerateSmallArray(tasks, T, n) { + castS(t, T); + // remove task from tasks to reorder tgroup + if (eqG(ssGet(t)+ORDER_ID_LENGTH, tid)) { + strcpy(task_in_group, ssGet(t)); + delG(tasks, n, n+1); + break; + } + finishG(t); + } + if (isEmptyG(task_in_group)) { + sprintf(return_move_task_to_a_group, "%s not found in %s", tid, tgroup); + return return_move_task_to_a_group; + } + + // Move group in tree + if (is_this_task_a_group(tid)) { + char treePath[8192]; + strcpy(treePath, find_group_in_tree(group)); + char *treeTid = strdup(find_group_in_tree(tid)); + sliceG(&treeTid, 0, -ID_LENGTH-1); + if (eqG(treeTid, treePath)) { + // prevent moving a group on itself, in tree and moving group title position to parent group + free(treeTid); + return tid; + } + free(treeTid); + shRename(find_group_in_tree(tid), treePath); + } + + // Remove task in source group + sprintf(path, "%s/%s", path, task_in_group); + rmAll(path); + + //:reorder_tasks + // Move tasks + {enumerateSmallArray(tasks, T, n) { + castS(t, T); + char src[8192]; + sprintf(src, "%s/%s", path, ssGet(t)); + char dst[8192]; + sprintf(dst, "%s/%s%s", path, baseconvert(n), ssGet(t)+ORDER_ID_LENGTH); + shRename(src, dst); + finishG(t); + }} + terminateG(tasks); + + // check that tid is not already linked in destination group + if (is_linked(tid)) { + createTaskPath(gPath, "groups/"); + smallArrayt *link_groups = readDirG(rtSmallArrayt, gPath); + if (hasG(link_groups, group)) { + // removed task reference from tgroup. Remove tgroup reference in task. There is already a tid reference in group, nothing more to do + char p[8192]; + sprintf(p, "%s/%s", gPath, tgroup); + rmAllG(p); + terminateG(link_groups); + return tid; + } + terminateG(link_groups); + } + + // Create reference in destination group + add_task_to_group_folder(tid, group); + + // linked tasks, remove source group and add destination group + if (is_linked(tid)) { + // remove source tgroup from tid/groups + createTaskPath(gPath, "groups/"); + char p[8192]; + sprintf(p, "%s/%s", gPath, tgroup); + rmAllG(p); + sprintf(p, "%s/%s", gPath, group); + FILE *f = fopen(p, "w"); + fclose(f); + } + + char log[256]; + sprintf(log, "moved %s in group %s to group %s", tid, tgroup, group); + edi_log(log); + return tid; +} + +/** string returned from copy_task_to_a_group */ +static char return_copy_task_to_a_group[ORDER_ID_LENGTH + ID_LENGTH +1]; + +/** copy task to group with new tid + * @return new tid + * @param[in] tid task id + * @param[in] group task id, destination group + * @ingroup EDI_CORE + */ +const char *copy_task_to_a_group(const char *tid, const char *group) { + // Generate new tid + strcpy(return_copy_task_to_a_group, generate_id()); + char *newtid = return_copy_task_to_a_group; + // Copy task in tasks + char newtidPath[8192]; + strcpy(newtidPath, generate_task_path(newtid)); + // add / to copy files in task + char *tPath = (char*) generate_task_path(tid); + strcat(tPath, "/"); + copy(tPath, newtidPath); + // delete tid/groups because new task is not linked + char task_linked_groups_path[8192]; + sprintf(task_linked_groups_path, "%s/groups/", newtidPath); + if (fileExistsG(task_linked_groups_path)) { + rmAllG(task_linked_groups_path); + } + // Copy group in groups and in tree + if (is_this_task_a_group(tid)) { + // create new group + copy(generate_group_path(tid), newtidPath); + // Change group title task to newtid + char tPath[8192]; + sprintf(tPath, "%s/%s%s", newtidPath, baseconvert(0), tid); + char ntPath[8192]; + sprintf(tPath, "%s/%s%s", newtidPath, baseconvert(0), newtid); + shRename(tPath, ntPath); + + // delete tasks to be recreated with new tid + char p[8192]; + smallArrayt *tasks = readDirG(rtSmallArrayt ,generate_group_path(newtid)); + forEachSmallArray(tasks, T) { + castS(t, T); + sprintf(p, "%s/%s", generate_group_path(newtid), ssGet(t)); + rmAllG(p); + finishG(t); + } + terminateG(tasks); + + // Add group in tree + if (eqG(group, "root")) { + sprintf(p, "%s/%s", data_location_tree, newtid); + mkdirParentsG(p); + } + else { + sprintf(p, "%s/%s", find_group_in_tree(group), newtid); + } + mkdirParentsG(p); + } + + + // Add reference in group + // Create reference in destination group + add_task_to_group_folder(newtid, group); + + if (is_this_task_a_group(tid)) { + // walk in group + // list items in group + smallArrayt *tasks = readDirG(rtSmallArrayt, generate_group_path(tid)); + + // add group found in first group + forEachSmallArray(tasks, T) { + castS(t, T); + // Check tasks that are not title task in a group + if (not eqG(ssGet(t) + ORDER_ID_LENGTH, tid)) + // copy_task_to_a_group recursively + copy_task_to_a_group(ssGet(t) + ORDER_ID_LENGTH,newtid); + finishG(t); + } + terminateG(tasks); + } + + sprintf(newtidPath, "copied %s to group %s, created %s", tid,group,newtid); + edi_log(newtidPath); + return newtid; +} + +/** string returned from generate_task_path_in_database */ +static char return_generate_task_path_in_database[8192]; + +/** create task path in database using database name + * @return path to task in tasks + * @param[in] tid task id + * @param[in] location database name in data section of easydoneit.ini + * @ingroup EDI_CORE + */ +const char *generate_task_path_in_database(const char *tid, const char *location) { + char *locPath = getG(selected_d, rtChar, location); + sprintf(return_generate_task_path_in_database, "%s/tasks/%s/", locPath, tid); + return return_generate_task_path_in_database; +} + +/** string returned from generate_group_path_in_database */ +static char return_generate_group_path_in_database[8192]; + +/** create group path in database using database name + * @return path to group in groups + * @param[in] tid task id + * @param[in] location database name in data section of easydoneit.ini + * @ingroup EDI_CORE + */ +const char *generate_group_path_in_database(const char *tid, const char *location) { + char *locPath = getG(selected_d, rtChar, location); + sprintf(return_generate_group_path_in_database, "%s/groups/%s/", locPath, tid); + return return_generate_group_path_in_database; +} + +/** string returned from find_group_in_tree_in_database */ +static char return_find_group_in_tree_in_database[8192]; + +/** find group in tree folder in database + * @return path to group in tree + * @param[in] group task id + * @param[in] location database name in data section of easydoneit.ini + * @ingroup EDI_CORE + */ +const char *find_group_in_tree_in_database(const char *group, const char *location) { + char location_tree[8192]; + char *locPath = getG(selected_d, rtChar, location); + sprintf(location_tree, "%s/tree", locPath); + char *path_in_tree = return_find_group_in_tree_in_database; + path_in_tree[0] = 0; + + if (eqG(group, "root")) { + strcpy(return_find_group_in_tree_in_database, location_tree); + } + else { + // list all group paths in tree + /* if platform.system() = 'Windows' */ + /* folders = [] */ + /* folders append location_tree */ + /* for dir, subdirs, files in os.walk(location_tree) */ + /* for sd in subdirs */ + /* folders append dir+os.sep + sd */ + /* else */ + smallArrayt *folders = walkDirAllG(rtSmallArrayt, location_tree); + enumerateSmallArray(folders, F, i) { + castS(f, F); + trimG(f); + setNFreePG(folders, i, f); + } + smallArrayt *groups = folders; + // find the group in group paths + forEachSmallArray(groups, G) { + castS(g, G); + char *s = basename(ssGet(g)); + if (eqG(s, group)) + strcpy(path_in_tree, ssGet(g)); + finishG(g); + } + terminateG(folders); + } + return path_in_tree; +} + +/** add task with new tid in selected database + * @param[in] tid task id + * @param[in] group task id + * @param[in] location database name in data section of easydoneit.ini + * @ingroup EDI_CORE + */ +void add_task_to_group_folder_in_database(const char *tid, const char *group, const char* location) { + // Create an entry in group + smallArrayt *tasks = readDirG(rtSmallArrayt, generate_group_path_in_database(group,location)); + // Add +1 to last order_id to have the task last in the list + const char *order_id; + if (lenG(tasks)) { + smallStringt *s = getG(tasks, rtSmallStringt, -1); + smallStringt *pos = copyRngG(s, 0, ORDER_ID_LENGTH); + finishG(s); + order_id = baseconvert(baseconvert_to_dec(ssGet(pos))+1); + terminateG(pos); + } + else { + // start at 0 when group is empty + order_id = baseconvert(0); + } + terminateG(tasks); + char p[8192]; + sprintf(p, "%s/%s%s", generate_group_path_in_database(group,location), order_id, tid); + FILE *f = fopen(p, "w"); + fclose(f); +} + +/** string returned from copy_task_to_database */ +static char return_copy_task_to_database[ORDER_ID_LENGTH + ID_LENGTH +1]; + +/** copy task to group in selected database with new tid + * @return new tid + * @param[in] tid task id + * @param[in] group task id + * @param[in] location database name in data section of easydoneit.ini + * @ingroup EDI_CORE + */ +const char *copy_task_to_database(const char *tid, const char *location, const char *group) { + // Generate new tid + char *newtid; + newtid = return_copy_task_to_database; + strcpy(newtid, generate_id()); + // Copy task in tasks + // add / to copy files in task + char *tidPath = (char*) generate_task_path(tid); + strcat(tidPath, "/"); + copy(tidPath,generate_task_path_in_database(newtid,location)); + // delete tid/groups because new task is not linked + char task_linked_groups_path[8192]; + sprintf(task_linked_groups_path, "%s/groups/", generate_task_path_in_database(newtid,location)); + if (fileExists(task_linked_groups_path)) { + rmAllG(task_linked_groups_path); + } + // Copy group in groups and in tree + if (is_this_task_a_group(tid)) { + // create new group + // add / to copy files in task + tidPath = (char*) generate_task_path(tid); + strcat(tidPath, "/"); + copy(tidPath,generate_group_path_in_database(newtid,location)); + // Change group title task to newtid + char tPath[8192]; + sprintf(tPath, "%s/%s%s", generate_group_path_in_database(newtid,location), baseconvert(0), tid); + char ntPath[8192]; + sprintf(ntPath, "%s/%s%s", generate_group_path_in_database(newtid,location), baseconvert(0), newtid); + shRename(tPath, ntPath); + + // delete tasks to be recreated with new tid + smallArrayt *tasks = readDirG(rtSmallArrayt, generate_group_path_in_database(newtid,location)); + forEachSmallArray(tasks, T) { + castS(t, T); + sprintf(tPath, "%s/%s", generate_group_path_in_database(newtid,location), ssGet(t)); + rmAll(tPath); + finishG(t); + } + terminateG(tasks); + + // Add group in tree + char location_tree[8192]; + char *locPath = getG(selected_d, rtChar, location); + sprintf(location_tree, "%s/tree", locPath); + if (eqG(group, "root")) { + sprintf(tPath, "%s/%s", location_tree, newtid); + mkdirParentsG(tPath); + } + else { + sprintf(tPath, "%s/%s", find_group_in_tree_in_database(group,location), newtid); + mkdirParentsG(tPath); + } + } + + + // Add reference in group + // Create reference in destination group + add_task_to_group_folder_in_database(newtid, group, location); + + if (is_this_task_a_group(tid)) { + // walk in group + // list items in group + smallArrayt *tasks = readDirG(rtSmallArrayt, generate_group_path(tid)); + + // add group found in first group + forEachSmallArray(tasks, T) { + castS(t, T); + // Check tasks that are not title task in a group + if (eqG(ssGet(t)+ORDER_ID_LENGTH, tid)) { + // copy_task_to_database recursively + copy_task_to_database(ssGet(t)+ORDER_ID_LENGTH,location,newtid); + } + finishG(t); + } + terminateG(tasks); + } + return newtid; +} + +/** move task to database/group + * @param[in] tgroup group id for task tid + * @param[in] tid task id + * @param[in] location database name in data section of easydoneit.ini + * @param[in] group destination group id + * @ingroup EDI_CORE + * copy and delete + */ +const char *move_task_to_a_group_to_database(const char *tgroup, const char *tid, const char *location, const char *group) { + const char *newtid = copy_task_to_database(tid,location,group); + if (is_this_task_a_group(tid)) { + delete_group(tid); + } + else { + delete_linked_task(tgroup,tid); + } + return newtid; +} + +/** add reference to tid in group + * @param[in] tid task id + * @param[in] group destination group id + * @ingroup EDI_CORE + * Used in edi ln to link tasks + */ +void add_task_reference_to_a_group(const char *tid, smallStringt *group) { + char p[8192]; + // Check if selected item is a task + if (not is_this_task_a_group(tid)) { + // add group to task folder + createTaskPath(task_path, "groups"); + FILE *f; + if (not fileExists(task_path)) { + mkdirParentsG(task_path); + + // add first group when task is linked to tid/groups/ + const char *first_group = find_group_containing_task(tid); + sprintf(p, "%s/%s", task_path, first_group); + f = fopen(p, "w"); + fclose(f); + } + sprintf(p, "%s/%s", task_path, ssGet(group)); + f = fopen(p, "w"); + fclose(f); + + // add task to group + add_task_to_group_folder(tid, ssGet(group)); + } + + sprintf(p, "linked %s to group %s", tid, ssGet(group)); + edi_log(p); +} + +/** set status for tid + * @param[in] tid task id + * @param[in] status_number index in edi_core.TASK_STATUS + * @ingroup EDI_CORE + */ +void set_status(const char *tid, i16 status_number) { + // Change status + createTaskPath(tpath, "status"); + writeFileG(TASK_STATUS[status_number], tpath); + sprintf(tpath, "set status for %s to %s", tid, TASK_STATUS_TRIM[status_number]); + edi_log(tpath); +} + +/* ## set all tasks in group to active */ +/* # @param[in] tid task id */ +/* # @ingroup EDI_CORE */ +/* def reset_group_status tid */ +/* group_tasks = os.listdir(generate_group_path(tid)) */ +/* for t in group_tasks */ +/* 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])) */ +/* set_status(t[ORDER_ID_LENGTH:],TASK_STATUS_ACTIVE) */ +/* edi_log('reset group %s'%tid) */ +/* */ +/** search string in tasks folder + * @param[in] search query string + * @ingroup EDI_CORE + */ +smallArrayt *search_string(const char *search){ + smallArrayt *tasks = walkDirG(rtSmallArrayt, data_location_tasks); + + // search in descriptions only + createAllocateSmallArray(grep_r); + forEachSmallArray(tasks, TID) { + castS(tido, TID); + if (not eqG(tido, "root")) { + // search in visible tasks only (filter enable status) + const char *task_status = get_status(ssGet(tido)); + if (getG(status_filters_d, rtI32, task_status) == ENABLE) { + /* if platform.system() = 'Windows' */ + /* // in windows, grep in python */ + /* f = open generate_task_path(tid)+os.sep+'description.txt' */ + /* f_lines = f readlines */ + /* f close */ + /* grep_lines = [] */ + /* for l in f_lines */ + /* if search lower in l lower */ + /* grep_lines append l */ + /* else */ + char *tid = ssGet(tido); + createTaskPath(dPath, "description.txt"); + createAllocateSmallArray(grep_lines); + char c[16384]; + sprintf(c, "grep -i \"%s\" %s", search, dPath); + execO(c, grep_lines, NULL); + // add tid and filename to results + sprintf(dPath, "/%s/description.txt:", tid); + forEachSmallArray(grep_lines, H) { + castS(h, H); + createAllocateSmallArray(a); + pushG(a, dPath); + pushNFreeG(a, h); + pushG(a, tid); + pushNFreeG(grep_r, a); + } + terminateG(grep_lines); + } + } + finishG(tido); + } + + // r lists all hits + createAllocateSmallArray(r); + forEachSmallArray(grep_r, L) { + cast(smallArrayt *, l_l, L); + // replace filename with tid and title (first line in file) + // read first line in file + char *hit_filename = getG(l_l, rtChar, 0); + char task_path[8192]; + sprintf(task_path, "%s/%s", data_location_tasks, hit_filename); + FILE *f = fopen(task_path, "r"); + char *title = readLineG(rtChar, f); + trimG(&title); + fclose(f); + // set tid + char *tid = getG(l_l, rtChar, 2); + char *group = " "; + if (is_this_task_a_group(tid)) { + group = "GROUP"; + } + if (is_linked(tid)) { + group = " LINK"; + } + char l[16384]; + sprintf(l, "%s %s - %s:%s", tid,group,title, getG(l_l, rtChar, 1)); + free(title); + pushG(r, l); + finishG(l_l); + } + terminateG(grep_r); + + return r; +} + +/** search string in group + * @param[in] group task id + * @param[in] search query string + * @ingroup EDI_CORE + */ +smallArrayt *search_string_in_group(const char *group, const char *search) { + + smallArrayt *tasks = readDirG(rtSmallArrayt, generate_group_path(group)); + + // r lists all hits + createAllocateSmallArray(r); + forEachSmallArray(tasks, O) { + castS(orderidtid, O); + // tid for current task in group + char *tid = ssGet(orderidtid)+ORDER_ID_LENGTH; + // search in visible tasks only (filter enable status) + const char *task_status = get_status(tid); + createAllocateSmallArray(grep_r); + if (getG(status_filters_d, rtI32, task_status) == ENABLE) { + /* if platform.system() = 'Windows' */ + /* // in windows, grep in python */ + /* f = open generate_task_path(tid)+os.sep+'description.txt' */ + /* f_lines = f readlines */ + /* f close */ + /* grep_lines = [] */ + /* for l in f_lines */ + /* if search lower in l lower */ + /* grep_lines append l */ + /* else */ + createTaskPath(dPath, "description.txt"); + createAllocateSmallArray(grep_lines); + char c[16384]; + sprintf(c, "grep -i \"%s\" %s", search, dPath); + execO(c, grep_lines, NULL); + // add tid and filename to results + sprintf(dPath, "/%s/description.txt", tid); + forEachSmallArray(grep_lines, H) { + castS(h, H); + createAllocateSmallArray(a); + pushG(a, dPath); + pushG(a, ssGet(h)); + pushG(a, tid); + pushNFreeG(grep_r, a); + finishG(h); + } + terminateG(grep_lines); + } + + forEachSmallArray(grep_r, L) { + cast(smallArrayt *, l_l, L); + // replace filename with tid and title (first line in file) + // read first line in file + char *hit_filename = getG(l_l, rtChar, 0); + char task_path[8192]; + sprintf(task_path, "%s/%s", data_location_tasks, hit_filename); + FILE *f = fopen(task_path, "r"); + if (!f) { + pFuncError + shEPrintfS("The path was: \"%s\"\n", task_path); + } + char *title = readLineG(rtChar, f); + trimG(&title); + fclose(f); + // set tid + tid = getG(l_l, rtChar, 2); + char *group = " "; + if (is_this_task_a_group(tid)) { + group = "GROUP"; + } + if (is_linked(tid)) { + group = " LINK"; + } + char l[16384]; + sprintf(l, "%s %s - %s:%s", tid,group,title, getG(l_l, rtChar, 1)); + free(title); + pushG(r, l); + finishG(l_l); + } + terminateG(grep_r); + finishG(orderidtid); + } + terminateG(tasks); + return r; +} + +/** search string in tree + * @param[in] group task id + * @param[in] search query string + * @ingroup EDI_CORE + */ +smallArrayt *search_string_in_tree(const char *group, const char *search) { + // walk_group is the list of groups to visit. FIFO + createAllocateSmallArray(r); + createAllocateSmallArray(walk_group); + pushG(walk_group, group); + // the while loop goes through all the group that are found + while (lenG(walk_group)) { + // list items in first group + smallArrayt *tasks = readDirG(rtSmallArrayt ,generate_group_path(getG(walk_group, rtChar, 0))); + appendNSmashG(r, search_string_in_group(getG(walk_group, rtChar,0), search)); + + // add group found in first group + forEachSmallArray(tasks, T) { + castS(t, T); + // Check tasks that are not title task in a group + if (not eqG(ssGet(t)+ORDER_ID_LENGTH, getG(walk_group, rtChar, 0)) and is_this_task_a_group(ssGet(t) + ORDER_ID_LENGTH)) { + pushG(walk_group, ssGet(t)+ORDER_ID_LENGTH); + } + finishG(t); + } + terminateG(tasks); + + // remove first group to list items in next group + delG(walk_group, 0, 1); + } + terminateG(walk_group); + return r; +} + +/* ## build tree recursively */ +/* # @param[in] group group path in tree starting with os.sep */ +/* # @return group_tree list of groups in tree starting from group */ +/* # @ingroup EDI_CORE */ +/* def build_group_tree group */ +/* # list groups */ +/* # call build_group_tree recursively */ +/* */ +/* # list groups */ +/* group_path = generate_group_path(group.split(os.sep)[-1]) */ +/* */ +/* groups = sorted(os.listdir(group_path)) */ +/* */ +/* if group != 'root' */ +/* # first task is group title */ +/* del groups[0] */ +/* else */ +/* # empty string for root */ +/* group = '' */ +/* */ +/* only_groups = [] */ +/* for g in groups */ +/* if is_this_task_a_group(g[ORDER_ID_LENGTH:]) */ +/* # complete tree path in only_groups */ +/* only_groups append '%s%s%s' % (group, os.sep, g[ORDER_ID_LENGTH:]) */ +/* */ +/* # call build_group_tree recursively */ +/* group_tree = [] */ +/* if only_groups */ +/* for g in only_groups */ +/* group_tree append g */ +/* group_tree += build_group_tree(g) */ +/* return group_tree */ +/* */ +/* ## build tree in order */ +/* # @param[in] group group path in tree or root */ +/* # @return group_tree list of groups in tree starting from group */ +/* # @ingroup EDI_CORE */ +/* def build_tree group */ +/* tidtree = build_group_tree(group) */ +/* return tidtree */ +/* */ +/* ## show tree */ +/* # @ingroup EDI_CORE */ +/* # print all trees: group tids and titles */ +/* def show_tree */ +/* r = [] */ +/* */ +/* tidtree = build_tree('root') */ +/* */ +/* # remove '/' from path */ +/* tidtree = [i[1:] for i in tidtree] */ +/* */ +/* # print titles path\n group tid path\n\n */ +/* for l in tidtree */ +/* # find title for group tids */ +/* group_titles_in_path = [] */ +/* for g in l strip.split(os.sep) */ +/* group_titles_in_path append get_task_title(g) */ +/* if user_interface = 'web' */ +/* # convert / to - to be able to create links correctly */ +/* group_titles_in_path = [i.replace(os.sep, '-') for i in group_titles_in_path] */ +/* # create string title/title... */ +/* group_titles_in_path_s = '/'.join(group_titles_in_path) */ +/* r append '%s\n'%group_titles_in_path_s */ +/* r append '%s\n'%l */ +/* */ +/* return r */ +/* */ +/* ## group statistics */ +/* # @ingroup EDI_CORE */ +/* # print statistics for a group recursively */ +/* def group_statistics group */ +/* global stats */ +/* global stats_total */ +/* global stats_creation_dates */ +/* global stats_overtime */ +/* */ +/* # compute total number of tasks in group */ +/* path = generate_group_path(group) */ +/* */ +/* tasks = [] */ +/* # remove group title */ +/* for i in os.listdir(path) */ +/* if not baseconvert(0) in i[:ORDER_ID_LENGTH] */ +/* tasks append i[ORDER_ID_LENGTH:] */ +/* */ +/* stats_total += len(tasks) */ +/* */ +/* # compute number of tasks in each state, groups and links */ +/* for tid in tasks */ +/* task_status = get_status(tid) */ +/* stats[task_status] += 1 */ +/* if is_this_task_a_group(tid) */ +/* stats[' Group'] += 1 */ +/* group_statistics(tid) */ +/* if is_linked(tid) */ +/* stats[' Linked'] += 1 */ +/* */ +/* #stats_creation_dates */ +/* # Figure out creation time and modification time for description */ +/* task_ctime = get_creation_date(tid)[1] */ +/* */ +/* task_path = generate_task_path(tid) */ +/* (mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime) = os.stat(task_path+os.sep+'description.txt') */ +/* # 'if' below to be compatible with first database format */ +/* if task_ctime = 0 */ +/* # ctime not available */ +/* task_ctime = mtime */ +/* stats_creation_dates append task_ctime */ +/* */ +/* (mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime) = os.stat(task_path+os.sep+'status') */ +/* */ +/* cdate = time.strftime("%Y-%m-%d", time.localtime(task_ctime)) */ +/* sdate = time.strftime("%Y-%m-%d", time.localtime(mtime)) */ +/* if not stats_overtime.has_key(cdate) */ +/* # initialize date dict */ +/* stats_overtime[cdate] = dict(zip(STATS_OVERTIME_KEYS,[0 for i in range(len(STATS_OVERTIME_KEYS))])) */ +/* stats_overtime[cdate]['Creation'] += 1 */ +/* if not stats_overtime.has_key(sdate) */ +/* # initialize date dict */ +/* stats_overtime[sdate] = dict(zip(STATS_OVERTIME_KEYS,[0 for i in range(len(STATS_OVERTIME_KEYS))])) */ +/* stats_overtime[sdate][task_status] += 1 */ +/* #end */ +/* */ +/* ## statistics */ +/* # @ingroup EDI_CORE */ +/* # print statistics for a group or a database */ +/* # compute speed */ +/* def statistics group */ +/* global stats */ +/* global stats_total */ +/* global stats_creation_dates */ +/* global stats_overtime */ +/* r = [] */ +/* */ +/* # initialize stats dictionary */ +/* stat_keys = [TASK_STATUS[i] for i in range(len(TASK_STATUS))] */ +/* stat_keys append ' Group' */ +/* stat_keys append ' Linked' */ +/* state_amounts = [0 for i in range(len(stat_keys))] */ +/* stats = dict(zip(stat_keys,state_amounts)) */ +/* */ +/* if group = 'for database' */ +/* # compute total number of tasks, excluding root */ +/* path = data_location_tasks */ +/* tasks = [] */ +/* for i in os.listdir(path) */ +/* if i != 'root' */ +/* tasks append i */ +/* */ +/* # compute number of tasks in each state, groups and links */ +/* for tid in tasks */ +/* task_status = get_status(tid) */ +/* stats[task_status] += 1 */ +/* if is_this_task_a_group(tid) */ +/* stats[' Group'] += 1 */ +/* if is_linked(tid) */ +/* stats[' Linked'] += 1 */ +/* */ +/* #:define stats_creation_dates */ +/* # Figure out creation time and modification time for description */ +/* task_ctime = get_creation_date(tid)[1] */ +/* */ +/* task_path = generate_task_path(tid) */ +/* (mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime) = os.stat(task_path+os.sep+'description.txt') */ +/* if task_ctime = 0 */ +/* # ctime not available */ +/* task_ctime = mtime */ +/* stats_creation_dates append task_ctime */ +/* */ +/* (mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime) = os.stat(task_path+os.sep+'status') */ +/* */ +/* cdate = time.strftime("%Y-%m-%d", time.localtime(task_ctime)) */ +/* sdate = time.strftime("%Y-%m-%d", time.localtime(mtime)) */ +/* if not stats_overtime.has_key(cdate) */ +/* # initialize date dict */ +/* stats_overtime[cdate] = dict(zip(STATS_OVERTIME_KEYS,[0 for i in range(len(STATS_OVERTIME_KEYS))])) */ +/* stats_overtime[cdate]['Creation'] += 1 */ +/* if not stats_overtime.has_key(sdate) */ +/* # initialize date dict */ +/* stats_overtime[sdate] = dict(zip(STATS_OVERTIME_KEYS,[0 for i in range(len(STATS_OVERTIME_KEYS))])) */ +/* stats_overtime[sdate][task_status] += 1 */ +/* #:end */ +/* */ +/* stats_total = len(tasks) */ +/* else */ +/* # compute total number of tasks in group */ +/* path = generate_group_path(group) */ +/* */ +/* tasks = [] */ +/* for i in os.listdir(path) */ +/* if group = 'root' */ +/* tasks append i[ORDER_ID_LENGTH:] */ +/* else */ +/* # the group task is not counted in the statistics */ +/* if not baseconvert(0) in i[:ORDER_ID_LENGTH] */ +/* tasks append i[ORDER_ID_LENGTH:] */ +/* */ +/* stats_total = len(tasks) */ +/* */ +/* # compute number of tasks in each state, groups and links, recursively */ +/* for tid in tasks */ +/* task_status = get_status(tid) */ +/* stats[task_status] += 1 */ +/* if is_this_task_a_group(tid) */ +/* stats[' Group'] += 1 */ +/* group_statistics(tid) */ +/* if is_linked(tid) */ +/* stats[' Linked'] += 1 */ +/* */ +/* #:stats_creation_dates */ +/* */ +/* if not stats_total */ +/* r append '0 task in statistics.' */ +/* return r */ +/* */ +/* # csv format, not used for now */ +/* #col_names = ['Tasks'] + sorted(stats.keys()) */ +/* #col_names = [i strip for i in col_names] */ +/* #print ','.join(col_names) */ +/* #csv = '%d'%len(tasks) */ +/* #for k in sorted(stats.keys()) */ +/* # csv += ',%d'%stats[k] */ +/* #r append csv */ +/* #r append '' */ +/* */ +/* r append 'Number of items: %d\n'%stats_total */ +/* for k in sorted(stats.keys()) */ +/* r append '%s - %d'%(k, stats[k]) */ +/* r append '' */ +/* */ +/* # speed */ +/* start_time = sorted(stats_creation_dates)[0] */ +/* now = time.time() */ +/* done_and_inactive = stats[TASK_STATUS[TASK_STATUS_DONE]] + stats[TASK_STATUS[TASK_STATUS_INACTIVE]] */ +/* if not done_and_inactive */ +/* r append 'Nothing is done or inactive' */ +/* else */ +/* # subtract tasks in state void, because tasks in void state are informative or groups */ +/* number_of_tasks = stats_total - stats[TASK_STATUS[TASK_STATUS_VOID]] */ +/* remaining_tasks = number_of_tasks - done_and_inactive */ +/* remaining_time = (now - start_time) / float(done_and_inactive) * remaining_tasks */ +/* r append 'Number of tasks (excluding voids): %d'%number_of_tasks */ +/* r append 'Remaining tasks: %d'%remaining_tasks */ +/* r append 'Remaining days: %.3f'%(remaining_time/86400) */ +/* r append 'Start date: %s'%time.strftime("%Y-%m-%d", time.localtime(start_time)) */ +/* r append "Today's date: %s"%time.strftime("%Y-%m-%d", time.localtime(now)) */ +/* r append 'Finish date: %s'%time.strftime("%Y-%m-%d", time.localtime(now + remaining_time)) */ +/* */ +/* # create csv stats from stats_overtime */ +/* f = open data_location+os.sep+'stats.csv' w */ +/* f write 'date,%s\n'%','.join([i strip for i in STATS_OVERTIME_KEYS]) */ +/* */ +/* for d in sorted(stats_overtime keys) */ +/* f write '%s,%s\n'%(d, ','.join([str(stats_overtime[d][i]) for i in STATS_OVERTIME_KEYS])) */ +/* f close */ +/* */ +/* # create creation, done and inactive stats */ +/* f = open data_location+os.sep+'stats_creation_done_inactive.csv' w */ +/* f write 'date,Creation,Done/Inactive\n' */ +/* */ +/* for d in sorted(stats_overtime keys) */ +/* 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]]) */ +/* f close */ +/* */ +/* r append '' */ +/* return r */ +/* */ +/* ## create secret */ +/* # @ingroup EDI_CORE */ +/* # encrypts description.txt in tid */ +/* # the title remains clear */ +/* def create_secret tid */ +/* */ +/* # encrypt description.txt to description.txt.gpg */ +/* tdir = generate_task_path(tid) +os.sep */ +/* path = tdir+'description.txt' */ +/* #:define encrypt */ +/* os.system 'gpg -c --cipher-algo AES256 %s' % path */ +/* # delete clear text */ +/* os.rename(path, tdir+'tmp') */ +/* # create clear title */ +/* os.system 'head -n 1 %s > %s' % (tdir+'tmp', path) */ +/* os.remove tdir+'tmp' */ +/* #:end */ +/* */ +/* ## edit secret */ +/* # @ingroup EDI_CORE */ +/* # decrypts description.txt.gpg to description.txt */ +/* # starts text editor */ +/* # encrypts description.txt in tid */ +/* # the title remains clear */ +/* def edit_secret tid */ +/* */ +/* # delete clear description.txt (holding the title) */ +/* tdir = generate_task_path(tid) +os.sep */ +/* path = tdir+'description.txt' */ +/* os.remove path */ +/* os.system 'gpg %s' % (path+'.gpg') */ +/* os.remove path+'.gpg' */ +/* # start text editor */ +/* edit_task(tid) */ +/* #:encrypt */ +/* */ +/* ## for your eyes only */ +/* # @ingroup EDI_CORE */ +/* # print secret and hide passwords */ +/* def fyeo tid */ +/* */ +/* # decrypt description.txt.gpg to stdout */ +/* f = os.popen 'gpg -d %s' % generate_task_path(tid) +os.sep+'description.txt.gpg' */ +/* t = f readlines */ +/* f close */ +/* for l in t */ +/* # hide text after the word password on lines starting with password */ +/* if l lower[:8] = 'password' */ +/* L = l strip.split(' ') */ +/* p = L[0] */ +/* del L[0] */ +/* s = ' '.join(L) */ +/* print '%s \033[1;8m\033[1;41m%s\033[0;0m' %(p, s) */ +/* else */ +/* # print normal text */ +/* print l rstrip */ +/* */ + +/** + * list text for all tasks in list + * @param + * tid group to show + * @return + * array containing all task descriptions in group + */ +smallArrayt *listTasksInList(smallArrayt *ls) { + createAllocateSmallArray(r); + forEachSmallArray(ls, T) { + cast(smallDictt*, d, T); + if (not eqG(getG(d, rtChar, "tid"), "")) { + smallArrayt *desc = display_task(getG(d, rtChar, "tid"), passThroughTitle); + pushG(r, "---"); + appendNSmashG(r, desc); + } + finishG(d); + } + return r; +} + +/** + * list text for all tasks in group tid + * @param + * tid group to show + * @return + * array containing all task descriptions in group + */ +smallArrayt *listTasksInGroup(const char *tid) { + createAllocateSmallArray(r); + if (is_this_task_a_group(tid)) { + // list_group... + smallArrayt *ls = list_group(tid); + forEachSmallArray(ls, T) { + cast(smallDictt*, d, T); + if (not eqG(getG(d, rtChar, "tid"), "")) { + smallArrayt *desc = display_task(getG(d, rtChar, "tid"), passThroughTitle); + pushG(r, "---"); + appendNSmashG(r, desc); + } + finishG(d); + } + terminateG(ls); + } + return r; +} + +/** + * list text in bookmarks + * @return + * array containing all task descriptions in the bookmarks + */ +smallArrayt *listTasksInBookmarks(void) { + createAllocateSmallArray(r); + forEachSmallArray(bookmarks, T) { + castS(s, T); + smallArrayt *desc = display_task(ssGet(s), passThroughTitle); + pushG(r, "---"); + appendNSmashG(r, desc); + finishG(s); + } + return r; +} + +/** clear edi_core buffers + * @ingroup EDI_DESKTOP + */ +void edi_core_clear_previous_run(void) { + emptyG(group_directory_file_list); + if (agenda) { + emptyG(agenda); + } +} + +/** search tid in all selected databases + * @return r empty when tid is not found, 'tid exists' when found + * @param[in] tid task or group id + * @ingroup EDI_DESKTOP + * The setup of the database for tid is kept + */ +bool tid_exists(const char *tid) { + bool r = false; + // clear edi_core variable because the database setup can change + edi_core_clear_previous_run(); + forEachSmallArray(selected_path, P) { + castS(path, P); + freeManyS(data_location, data_location_tasks, data_location_groups, data_location_tree); + data_location = dupG(ssGet(path)); + + data_location_tasks = appendG(data_location, "/tasks"); + data_location_groups = appendG(data_location, "/groups"); + data_location_tree = appendG(data_location, "/tree"); + + if (not fileExists(data_location)) { + //strcpy(result_select_database, data_location); + //strcat(result_select_database, " is unreachable"); + finishG(P); + break; + } + + if (fileExists(generate_task_path(tid))) { + r = true; + finishG(P); + break; + } + finishG(P); + } + return r; +} + +/** search tid in all selected databases + * @param[in] tid task or group id + * @ingroup EDI_DESKTOP + * The setup of the database for tid is kept. + * Same as tid_exists without return value. + */ +bool setup_data_location_for_tid(const char *tid) { + return tid_exists(tid); +} + +/** save current database path in saved_data_location + * @ingroup EDI_DESKTOP + */ +void save_edi_core_data_location(void) { + //debug print 'save' + free(saved_data_location); + saved_data_location = dupG(data_location); +} + +/** restore saved_data_location + * @ingroup EDI_DESKTOP + */ +void restore_edi_core_data_location(void) { + //debug print 'restore' + + freeManyS(data_location, data_location_tasks, data_location_groups, data_location_tree); + data_location = saved_data_location; + saved_data_location = NULL; + + data_location_tasks = appendG(data_location, "/tasks"); + data_location_groups = appendG(data_location, "/groups"); + data_location_tree = appendG(data_location, "/tree"); + + // clear edi_core variable because the database setup can change + edi_core_clear_previous_run(); +} + +char return_getDatabaseNameFromPath[8192]; + +const char *getDatabaseNameFromPath(const char *database_path) { + return_getDatabaseNameFromPath[0] = 0; + forEachSmallDict(selected_d, k, V) { + castS(v, V); + if (eqG(v, database_path)) { + strcpy(return_getDatabaseNameFromPath, k); + finishG(v); + break; + } + finishG(v); + } + listFreeS(libsheepyInternalKeys); + return return_getDatabaseNameFromPath; +} + +// test core functions +void test(void) { + // test + group_directory_file_list = walkDirG(rtSmallArrayt, data_location_groups); + logG(group_directory_file_list); + return; + smallArrayt *ls = list_group("root"); + + forEachSmallArray(ls, L) { + cast(smallDictt*, d, L) + putsG(d); + finishG(d); + } + forEachSmallArray(ls, L) { + cast(smallDictt*, d, L) + putsG(getG(d, rtChar, "title")); + finishG(d); + } + terminateG(ls); + + smallArrayt *desc = display_task("RUkjgkUSsDYREKqM", generate_task_string_with_tid); + logG(desc); + terminateG(desc); + + /* t1 = add_task('root', 'task1.txt') */ + /* add_task('root', 'task2.txt') */ + /* g3 = add_task('root', 'task3.txt') */ + /* */ + /* create_group(g3) */ + /* t4 = add_task(g3, 'task4.txt') */ + /* */ + /* list_group('root') */ + /* list_group(g3) */ + /* */ + /* //display_task(t1) */ + /* //display_task(t4) */ + /* */ + /* // Delete task in a group of 2 tasks */ + /* print '// Delete task in a group of 2 tasks' */ + /* delete_task(t4) */ + /* */ + /* list_group('root') */ + /* */ + /* create_group(g3) */ + /* t4 = add_task(g3, 'task4.txt') */ + /* */ + /* list_group('root') */ + /* */ + /* // Delete group first task */ + /* print '// Delete group first task' */ + /* delete_task(g3) */ + /* */ + /* list_group('root') */ + /* */ + /* // Delete group task of a group with more than 2 tasks */ + /* print '// Delete group task of a group with more than 2 tasks' */ + /* delete_task(t4) */ + /* */ + /* g3 = add_task('root', 'task3.txt') */ + /* create_group(g3) */ + /* t4 = add_task(g3, 'task4.txt') */ + /* t2 = add_task(g3, 'task2.txt') */ + /* */ + /* list_group('root') */ + /* list_group(g3) */ + /* new_group_id = delete_task(g3) */ + /* print 'New group id %s'%new_group_id */ + /* */ + /* list_group('root') */ + /* list_group(new_group_id) */ + /* */ + /* // Delete group */ + /* print '// Delete group' */ + /* delete_group(new_group_id) */ + /* */ + /* list_group('root') */ + /* */ + /* print baseconvert(100) */ + /* print baseconvert_to_dec(baseconvert(100)) */ + /* print */ + /* */ + /* //edit_task(t1) */ + /* */ + /* // Change order */ + /* print '// Change order' */ + /* g3 = add_task('root', 'task3.txt') */ + /* change_task_order('root',1,0) */ + /* change_task_order('root',0,2) */ + /* */ + /* list_group('root') */ + /* */ + /* // Change status */ + /* print '// Change status' */ + /* create_group(g3) */ + /* t4 = add_task(g3, 'task4.txt') */ + /* t2 = add_task(g3, 'task2.txt') */ + /* */ + /* list_group('root') */ + /* */ + /* set_status(t1,TASK_STATUS_DONE) */ + /* set_status(t4,TASK_STATUS_DONE) */ + /* */ + /* list_group('root') */ + /* list_group(g3) */ + /* */ + /* // Reset status */ + /* */ + /* reset_group_status(g3) */ + /* */ + /* list_group('root') */ + /* list_group(g3) */ + /* */ + /* reset_group_status('root') */ + /* */ + /* list_group('root') */ + /* list_group(g3) */ + /* */ + /* // Create a group in group */ + /* print '// Create a group in group' */ + /* create_group(t4) */ + /* t2 = add_task(t4,'task2.txt') */ + /* */ + /* list_group('root') */ + /* list_group(g3) */ + /* list_group(t4) */ + /* */ + /* // Delete task in a group of 2 tasks not in root */ + /* print '// Delete task in a group of 2 tasks not in root' */ + /* delete_task(t2) */ + /* */ + /* list_group('root') */ + /* list_group(g3) */ + /* */ + /* // Delete group with groups in it */ + /* print '// Delete group with groups in it' */ + /* create_group(t4) */ + /* t2 = add_task(t4,'task2.txt') */ + /* */ + /* list_group('root') */ + /* list_group(g3) */ + /* list_group(t4) */ + /* */ + /* delete_group(g3) */ + /* */ + /* list_group('root') */ + /* */ + /* // Search string */ + /* print '// Search string' */ + /* */ + /* search_string('AND') */ + /* */ + /* // List tree */ + /* print '// List tree' */ + /* g3 = add_task('root', 'task3.txt') */ + /* create_group(g3) */ + /* t4 = add_task(g3, 'task4.txt') */ + /* t2 = add_task(g3, 'task2.txt') */ + /* create_group(t4) */ + /* t2 = add_task(t4,'task2.txt') */ + /* */ + /* list_tree('root') */ + /* */ + /* // Show group for a task */ + /* print '// Show group for a task' */ + /* */ + /* print 'Show group for %s - %s' %(t2,get_task_title(t2)) */ + /* show_group_for_task(t2) */ + /* print 'Show group for %s - %s' %(t4,get_task_title(t4)) */ + /* show_group_for_task(t4) */ + /* print 'Show group for %s - %s' %(t1,get_task_title(t1)) */ + /* show_group_for_task(t1) */ + + // create task + //create_task(t4) + //list_group(t4) +} + +/** start - always called at startup. + * @ingroup EDI_CORE + * @param[in] interface selects current user interface. cli loads the configuration from user home, web loads the configuration located in edi_web folder. + * creates default .easydoneit.ini<br> + * creates database folders<br> + * loads .easydoneit.ini + */ +void start(char *interface) { + // steps + // initialize variables + // create default config + // load config from inipath + + // initialize variables + add_top_or_bottom = strdup("bottom"); + status_filters = allocG(rtSmallArrayt); + //static i16 no_color[4] = {-1,-1,-1,255}; + //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}}; + no_color = allocG(rtSmallArrayt); + pushG(no_color, -1); + pushG(no_color, -1); + pushG(no_color, -1); + pushG(no_color, 255); + status_fgColors = allocG(rtSmallArrayt); + smallArrayt *c = allocG(rtSmallArrayt); + pushG(c, 0); + pushG(c, 0); + pushG(c, 0); + pushG(c, 255); + pushNFreeG(status_fgColors, dupG(c)); + setG(c, 0, 0); + setG(c, 1, 255); + setG(c, 2, 0); + setG(c, 3, 255); + pushNFreeG(status_fgColors, dupG(c)); + setG(c, 0, 255); + setG(c, 1, 128); + setG(c, 2, 0); + setG(c, 3, 255); + pushNFreeG(status_fgColors, dupG(c)); + setG(c, 0, 255); + setG(c, 1, 0); + setG(c, 2, 0); + setG(c, 3, 255); + pushNFreeG(status_fgColors, dupG(c)); + setG(c, 0, 192); + setG(c, 1, 192); + setG(c, 2, 192); + setG(c, 3, 255); + pushNFreeG(status_fgColors, dupG(c)); + setG(c, 0, 0); + setG(c, 1, 0); + setG(c, 2, 0); + setG(c, 3, 255); + pushNFreeG(status_fgColors, dupG(c)); + status_bgColors = allocG(rtSmallArrayt); + range(i, COUNT_ELEMENTS(TASK_STATUS)-1) { + pushNFreeG(status_bgColors, dupG(no_color)); + } + //logVarG(no_color); + //logVarG(status_fgColors); + //logVarG(status_bgColors); + group_directory_file_list = allocG(rtSmallArrayt); + + inipath = "~/.easydoneit.ini"; + inipath = expandHomeG(inipath); + + user_interface = interface; + + if (not eqG(user_interface, "tui")) { + puts(user_interface); + puts(BLD RED "Not supported." RST); + XFAILURE + } + else { + if (not fileExists(inipath)) { + // create default config + createAllocateSmallArray(newcfg); + pushG(newcfg, "[data]"); + pushG(newcfg, "location=~/easydoneit_data"); + pushG(newcfg, "1=~/easydoneit_data"); + pushG(newcfg, ""); + pushG(newcfg, "[locations]"); + pushG(newcfg, "selected=1"); + pushG(newcfg, "default_add_in=1"); + pushG(newcfg, ""); + pushG(newcfg, "[filters]"); + // enable all status filters + char buf[1024]; + forEachS(TASK_STATUS, nfilter) { + // strip to remove spaces in status strings + char *filter = lowerS(nfilter); + trimG(&filter); + sprintf(buf, "%s= %s", filter, STATUS_FILTER_STATES[0]); + free(filter); + pushG(newcfg, buf); + } + pushG(newcfg, ""); + pushG(newcfg, "[colors]"); + enumerateS(TASK_STATUS, nfilter, n) { + char *filter = lowerS(nfilter); + trimG(&filter); + smallArrayt *c = getG(status_fgColors, rtSmallArrayt, n); + 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)); + finishG(c); + free(filter); + pushG(newcfg, buf); + } + {enumerateS(TASK_STATUS, nfilter, n) { + char *filter = lowerS(nfilter); + trimG(&filter); + smallArrayt *c = getG(status_bgColors, rtSmallArrayt, n); + 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)); + finishG(c); + free(filter); + pushG(newcfg, buf); + }} + + pushG(newcfg, "\n[settings]"); + pushG(newcfg, "editor=vi"); + + writeFileG(newcfg, inipath); + terminateG(newcfg); + } + } + + // load config from inipath + ini = parseIni(inipath); + + smallDictt *data = getG(ini, rtSmallDictt, "data"); + smallDictt *locations = getG(ini, rtSmallDictt, "locations"); + smallDictt *filters = getG(ini, rtSmallDictt, "filters"); + smallDictt *colors = getG(ini, rtSmallDictt, "colors"); + smallDictt *settings = getG(ini, rtSmallDictt, "settings"); + smallDictt *desktop = getG(ini, rtSmallDictt, "desktop"); + + //logVarG(filters); + + // convert ~ to home path + data_location = expandHomeG(getG(data, rtChar,"location")); + + // load available databases + char **data_keys = keysG(data); + smallArrayt *data_section = valuesG(data); + // remove location which is a path to a database + // clear databases for reentrant testing + databases = allocG(rtSmallArrayt); + enumerateSmallArray(data_section, D, i) { + if (not eqG(data_keys[i], "location")) { + pushNFreeG(databases, dupG(D)); + } + free(D); + } + //logVarG(databases); + + // load add_top_or_bottom + char **location_keys = keysG(locations); + if (hasG(location_keys, "add_top_or_bottom")) { + add_top_or_bottom = getNDupG(locations, rtChar,"add_top_or_bottom"); + } + + // load selected database paths + smallStringt *sselected = getNDupG(locations, rtSmallStringt, "selected"); + selected = splitG(sselected, ","); + terminateG(sselected); + default_add_in = getNDupG(locations,rtChar, "default_add_in"); + selected_path = allocG(rtSmallArrayt); + forEachSmallArray(selected, D) { + castS(d, D); + pushNFreeG(selected_path, expandHomeG(getG(data, rtChar, ssGet(d)))); + free(D); + } + + selected_d = allocG(rtSmallDictt); + zipG(selected_d, selected, selected_path); + //logVarG(selected_path); + //logVarG(selected_d); + + // load autolink groups + if (hasG(location_keys, "autolink")) { + smallStringt *autolink_cfg = getG(locations, rtSmallStringt,"autolink"); + autolink = allocG(rtSmallArrayt); + rangeStep(i, lenG(autolink_cfg), ID_LENGTH+1) { + smallStringt *s = copyRngG(autolink_cfg, i, i+ID_LENGTH); + pushNFreeG(autolink, s); + } + finishG(autolink_cfg); + } + //logVarG(autolink); + + // load list groups + if (hasG(location_keys, "list")) { + smallStringt *list_cfg = getG(locations, rtSmallStringt, "list"); + list_of_groups = splitG(list_cfg, ","); + } + + // load status filters and status colors + char **config_filter_names = keysG(filters); + char **config_color_names = keysG(colors); + enumerateS(TASK_STATUS, nfilter, n) { + // strip to remove spaces in status strings + // check that task_status filter is in config file, to avoid problems between config file versions + char *filter = lowerS(nfilter); + trimG(&filter); + pushG(&TASK_STATUS_TRIM, filter); + if (hasG(config_filter_names, filter)) { + smallStringt *s = getG(filters, rtSmallStringt, filter); + if (eqG(s, STATUS_FILTER_STATES[DISABLE])) { + pushG(status_filters, DISABLE); + } + else { + pushG(status_filters, ENABLE); + } + finishG(s); + } + char *fc = appendG(filter, "_fgcolor"); + if (hasG(config_color_names, fc)) { + smallStringt *s = getG(colors, rtSmallStringt, fc); + smallArrayt *a = splitG(s, ","); + + // convert a strings to int + enumerateSmallArray(a, CP, ci) { + castS(cp, CP); + setG(a, ci, parseIntG(cp)); + finishG(cp); + } + setNFreeG(status_fgColors, n, a); + finishG(s); + } + free(fc); + fc = appendG(filter, "_bgcolor"); + if (hasG(config_color_names, fc)) { + smallStringt *s = getG(colors, rtSmallStringt, fc); + smallArrayt *a = splitG(s, ","); + + // convert a strings to int + enumerateSmallArray(a, CP, ci) { + castS(cp, CP); + setG(a, ci, parseIntG(cp)); + finishG(cp); + } + setNFreeG(status_bgColors, n, a); + finishG(s); + } + free(fc); + } + //logVarG(status_fgColors); + //logVarG(status_bgColors); + + // Set text editor + editor = getNDupG(settings, rtChar,"editor"); + + // set user name and email + char **settings_keys = keysG(settings); + if (hasG(settings_keys, "username")) { + user = getNDupG(settings, rtChar, "username"); + } + else { + user = strdup("Unknown"); + } + if (hasG(settings, "useremail")) { + email = getNDupG(settings, rtChar, "useremail"); + } + else { + emptyS(email); + } + + if (desktop) { + smallStringt *s = getG(desktop, rtSmallStringt, "bookmarks"); + bookmarks = splitG(s, "|"); + finishG(s); + } + else { + // no desktop section in ini file, empty bookmarks + bookmarks = allocG(rtSmallArrayt); + } + + // init + init(); + + listFreeManyS(data_keys, location_keys, settings_keys, config_filter_names, config_color_names); + finishManyG(data, locations, filters, colors, settings, desktop); +} diff --git a/edCore.h b/edCore.h @@ -0,0 +1,71 @@ + +const char *create_task(const char *group); +void edit_task(const char *tid); +const char *delete_task(const char *tid); +bool is_linked(const char *tid); +const char *generate_task_path(const char *tid); +const char *find_group_containing_task(const char *tid); +const char *get_status(const char *tid); +bool is_this_task_a_group(const char *tid); +i64 baseconvert_to_dec(const char *n); +void add_task_reference_to_a_group(const char *tid, smallStringt *group); +smallArrayt *create_group(const char *tid); +const char *find_group_in_tree(const char *group); +const char *delete_linked_task(const char *group, const char *tid); +void set_status(const char *tid, i16 status_number); +void start(char *interface); +smallArrayt *list_group(const char *tid); +smallArrayt *listBookmarks(void); +smallArrayt *display_task(const char *tid, smallStringt *titleFunc(const char *, smallStringt *)); +smallStringt *passThroughTitle(const char *tid, smallStringt *s); +smallStringt *get_task_title(const char *tid); +const char *select_database(char *location); +smallArrayt *listTasksInList(smallArrayt *list); +smallArrayt *listTasksInGroup(const char *tid); +smallArrayt *listTasksInBookmarks(void); +smallArrayt *show_group_for_task(const char *tid); +smallArrayt *search_string_in_tree(const char *group, const char *search); +smallDictt *get_task_in_list_group_format(const char *tid); +smallArrayt *create_group(const char *tid); +const char *convert_group_to_task(const char *group); +const char *copy_task_to_a_group(const char *tid, const char *group); +const char *copy_task_to_database(const char *tid, const char *location, const char *group); +const char *move_task_to_a_group(const char *tgroup, const char *tid, const char *group); +const char *move_task_to_a_group_to_database(const char *tgroup, const char *tid, const char *location, const char *group); +bool tid_exists(const char *tid); +bool setup_data_location_for_tid(const char *tid); +void save_edi_core_data_location(void); +void restore_edi_core_data_location(void); +const char *getDatabaseNameFromPath(const char *database_path); + +void test(void); + +extern smallDictt *ini; +extern char *data_location; +extern char *saved_data_location; + +/** Length of task id */ +#define ID_LENGTH 16 + +extern char *add_top_or_bottom; + +extern smallArrayt *selected; + +extern char *TASK_STATUS[]; +extern char **TASK_STATUS_TRIM; +extern char *STATUS_FILTER_STATES[]; + +#define TASK_STATUS_ACTIVE 0 +#define TASK_STATUS_DONE 1 +#define TASK_STATUS_ONGOING 2 +#define TASK_STATUS_PENDING 3 +#define TASK_STATUS_INACTIVE 4 +#define TASK_STATUS_VOID 5 + +enum {ENABLE, DISABLE}; + +extern smallArrayt *status_filters; + +extern smallDictt *status_filters_d; + +extern smallArrayt *bookmarks; diff --git a/edt.c b/edt.c @@ -0,0 +1,1996 @@ +#! /usr/bin/env sheepy + +#include <stdio.h> +#include <stdlib.h> +#include <ncurses.h> +#include <unistd.h> +#include <signal.h> +#include "shpPackages/md/md.h" +#include "shpPackages/ini/src/ini.h" +#include "tui/tui.h" +#include "libsheepyObject.h" +#include "edCore.h" + +// TODO add n 'add many tasks' and m 'add many one line tasks' +// TODO sort group list +// TODO handle path in statusBar when it exceeds screen size + +/** path element + */ +typedef struct { + /** group id */ + char group[16+1]; + /** position in list */ + u16 pos; + /** index in list of first line in the window */ + u16 firstLineOnScreen; +} pathInTreeElement; + +/** maximum path length stored, when MAX_PATH_DEPTH is reached, the oldest path is removed */ +// TODO test with low MAX +#define MAX_PATH_DEPTH 1000 +/** path in tree circular buffer */ +staticArrayT(pathInTreeT, pathInTreeElement, MAX_PATH_DEPTH); +/** pathInTree has the path browsed without the group in WGROUP */ +pathInTreeT pathInTree; +/** titles for group in pathInTree and currentGroup for statusBar */ +smallArrayt *pathTitles = NULL; + +/** main window */ +WINDOW *mainwin; +/** sub windows (colummns) */ +void **subwins = NULL; +enum { WLEFT, WGROUP, WVIEW, WSEARCH, WHELP}; + +/** window attributes */ +typedef struct { + /** this windows index in subwins array */ + u8 subwin; + /** coordinate */ + i16 row; + /** coordinate */ + i16 col; + /** size */ + i16 szrow; + /** size */ + i16 szcol; + /** index in list of first line on screen */ + i32 indexFirstLine; +} awindow; + +/** window array for attributes */ +awindow windows[5]; + +/** screen size */ +int row,col; + +enum {SHOWING_DATABASE, SHOWING_BOOKMARKS, SEARCH, SHOWING_SEARCH, HELP}; +/** ui state for user input */ +i8 state = SHOWING_DATABASE; +i8 previousState; +/** state for Info (F9) function - toggle between info and text/group */ +bool stateInfo = false; + +/** group in the WGROUP window */ +char currentGroup[ID_LENGTH+1]; +/** cursor in the WGROUP window */ +char selectedGroup[ID_LENGTH+1]; +/** index of cursor in WLEFT window */ +u16 leftCursor = 0; +/** index if cursor in WGROUP */ +u16 selectedLine = 0; + +/** list of select items (tids) */ +smallArrayt *selectedList = NULL; +smallArrayt *selectedListAttributes = NULL; + +/** list in WLEFT window */ +smallArrayt *lsLeft = NULL; +/** list in WGROUP window */ +smallArrayt *lsGroup = NULL; +/** list in WVIEW window */ +smallArrayt *viewText = NULL; +bool viewTextIsGroup = false; + +const int cursorColor = 8; +const int regularListColor = 0; +const int groupListColor = 2; +const int selectedListColor = 3; + +char statusSymbols[] = "AD>^# "; +i8 statusColors[] = {0, 6, 3, 14, 4, 5}; +typedef enum { HIDE_STATUS, SHOW_STATUS} displayStatusT; + +char *searchString = NULL; + +typedef enum { NO_PAPER, COPIED, CUT} paperfunctionT; +paperfunctionT paperfunction = NO_PAPER; + +/* PROTOTYPES */ +void statusBar(void); + +/** + * add group to pathInTree with cursor position in group list + * @param + * groupid group id to add + * @param + * pos cursor position in group list + */ +void addGroupToPath(const char *groupid, u16 pos) { + if (staticArrayIsFull(pathInTree)) { + // remove oldest path when full + staticArrayDequeue(pathInTree); + } + + // allocate element + staticArrayPush(pathInTree); + // store groupid + strcpy(staticArrayLast(pathInTree).group, groupid); + // store cursor position + staticArrayLast(pathInTree).pos = pos; + // store window state + staticArrayLast(pathInTree).firstLineOnScreen = windows[WGROUP].indexFirstLine; + // copy window state WLEFT to keep same view as WGROUP + windows[WLEFT].indexFirstLine = windows[WGROUP].indexFirstLine; +} + +/** + * go back in path history + */ +void moveToParentGroup(void) { + if (not staticArrayIsEmpty(pathInTree)) { + // there is a parent + // set last path to currentGroup + strcpy(currentGroup, staticArrayLast(pathInTree).group); + // set cursor in WGROUP + selectedLine = staticArrayLast(pathInTree).pos; + // set first line index in window + windows[WGROUP].indexFirstLine = staticArrayLast(pathInTree).firstLineOnScreen; + // pop path + staticArrayPop(pathInTree); + if (not staticArrayIsEmpty(pathInTree)) { + // set first line in window for new parent path + windows[WLEFT].indexFirstLine = staticArrayLast(pathInTree).firstLineOnScreen; + } + } + // TODO handle situation when pathInTree is empty and currentGroup is not root (lost history in the circular buffer) +} + +/** get first group id in pathInTree */ +const char *getParentGroupInPath(void) { + if (not staticArrayIsEmpty(pathInTree)) { + // return last element in pathInTree + return staticArrayLast(pathInTree).group; + } + else { + // database top or pathInTree empty + return ""; + } +} + +/** create string with status + * @param + * d dictionary containing the information about the group + * @return + * statusIndex index in TASK_STATUS + * @return + * string to be freed in the format 'STATUS TITLE' + */ +char *statusAndTitle(smallDictt *d, i8 *statusIndex) { + // status is one char + char statusSymbol[3] = " "; + *statusIndex = 0; + + // convert status string to status index in TASK_STATUS array + for (; *statusIndex < TASK_STATUS_VOID+1 ; (*statusIndex)++) { + if (eqG(getG(d, rtChar, "status"), TASK_STATUS[*statusIndex])) { + break; + } + } + statusSymbol[0] = statusSymbols[*statusIndex]; + char *s = catS(statusSymbol, getG(d, rtChar, "title")); + return s; +} + +/** display list in window w with cursor and showing/hiding each task/group status */ +void displayGroupList(smallArrayt *list, u16 w, u16 cursor, displayStatusT status) { + if (lenG(list) <= windows[w].szrow) { + // the list is smaller than the window + // the list top is the first line in the window + windows[w].indexFirstLine = 0; + } + // when the list is longer than the window, check that the list end is at window bottom + #define checkEndListInWindow(list, w) ((lenG(list) > windows[w].szrow) and ((windows[w].indexFirstLine + windows[w].szrow) > lenG(list))) + if checkEndListInWindow(list, w) { + // keep list end at window bottom + windows[w].indexFirstLine = lenG(list) - windows[w].szrow -1; + } + // print list in window + enumerateSmallArray(list, L, line) { + cast(smallDictt*, d, L) + if (line < windows[w].indexFirstLine) { + // skip elements before first line on screen + goto end; + } + i8 i; + char *s; + if (status == HIDE_STATUS) { + // hide status for database list, use regularListColor + i = regularListColor; + } + else { + // color according to status + s = statusAndTitle(d, &i); + } + if (line == cursor) { + // show cursor in cursorColor + wcolor_set(subwins[w], cursorColor, NULL); + if (w == WGROUP) { + // get tid at cursor position in list + // only for list in WGROUP window + strcpy(selectedGroup, getG(d, rtChar, "tid")); + } + } + else { + // set color + if (hasG(selectedList, getG(d, rtChar, "tid"))) { + wcolor_set(subwins[w], selectedListColor, NULL); + } + else { + if (eqG(getG(d, rtChar, "group"), "GROUP")) { + wcolor_set(subwins[w], groupListColor, NULL); + } + else { + wcolor_set(subwins[w], statusColors[i], NULL); + } + } + } + if (status == HIDE_STATUS) { + // print title without status + mvwaddnstr(subwins[w], line - windows[w].indexFirstLine, 0, getG(d, rtChar, "title"), windows[w].szcol-1); + } + else { + // print title with status + mvwaddnstr(subwins[w], line - windows[w].indexFirstLine, 0, s, windows[w].szcol-1); + free(s); + } + if (line == cursor) { + // fill the cursor line with cursorColor + wchgat(subwins[w],-1, 0, cursorColor , NULL); + } +end: + finishG(d); + } +} + +/** show list in WLEFT window */ +void showLeft(void) { + if (not currentGroup[0]) { + // path is database list, show nothing + return; + } + if (getParentGroupInPath()[0]) { + // a group is displayed on the left + displayGroupList(lsLeft, WLEFT, leftCursor, SHOW_STATUS); + } + else { + // display database list without status + displayGroupList(lsLeft, WLEFT, leftCursor, HIDE_STATUS); + } +} + +/** + * generate database list + * @return + * ls array of dictionaries, same format as list_group + */ +void listDatabases(smallArrayt *ls) { + // list selected databases (from .easydoneit.ini) + enumerateSmallArray(selected, S, line) { + castS(s, S); + createAllocateSmallDict(task_d); + setG(task_d, "head", "element"); + setG(task_d, "tid", ""); + setG(task_d, "position", line); + setG(task_d, "group", "GROUP"); + setG(task_d, "title", ssGet(s)); + setG(task_d, "status", TASK_STATUS[TASK_STATUS_VOID]); + setG(task_d, "ctime", 0); + pushNFreeG(ls, task_d); + finishG(s); + } + createAllocateSmallDict(task_d); + setG(task_d, "head", "empty line"); + setG(task_d, "tid", ""); + setG(task_d, "position", 0); + setG(task_d, "group", ""); + setG(task_d, "title", ""); + setG(task_d, "status", ""); + setG(task_d, "ctime", 0); + pushNFreeG(ls, task_d); +} + +/** + * generate and display list in WLEFT window + * @param + * parent group id or "" for database root + */ +void viewLeftParent(const char *parent) { + if (parent[0] == 0) { + // database root + if (not currentGroup[0]) { + // path is database list, show nothing + // the database list is in WGROUP, the user is selecting a database + return; + } + // show database list + leftCursor = staticArrayGet(pathInTree, pathInTree.last-1).pos; + // create list + if (lsLeft) { + terminateG(lsLeft); + } + initiateAllocateSmallArray(&lsLeft); + listDatabases(lsLeft); + } + else { + // show parent group + if (lsLeft) { + terminateG(lsLeft); + } + lsLeft = list_group(parent); + // find currentGroup in list + // TODO add a pos parameter to viewLeftParent and remove this loop + enumerateSmallArray(lsLeft, L, pI) { + cast(smallDictt*, d, L) + if (eqG(getG(d, rtChar, "tid"), currentGroup)) { + break; + } + finishG(d); + } + leftCursor = pI; + } + showLeft(); +} + +/** show list in WGROUP window */ +void showGroup(void) { + if (lenG(lsGroup) == 1) { + // list is empty, set invalid selectedGroup tid, no cursor in list + // (lsGroup always has an empty item at the end of the list) + selectedGroup[0] = 0; + } + displayGroupList(lsGroup, WGROUP, selectedLine, SHOW_STATUS); +} + +/** + * generate and display list in WGROUP window + * @param + * currentGroup group id + */ +void viewGroup(const char *currentGroup) { + if (state == SHOWING_SEARCH) { + goto show; + } + if (lsGroup) { + terminateG(lsGroup); + } +ifState: + if (not currentGroup[0]) { + // path is database list + // show database list to select another database + initiateAllocateSmallArray(&lsGroup); + listDatabases(lsGroup); + } + else if (state == SHOWING_BOOKMARKS) { + lsGroup = listBookmarks(); + } + else if (state == SEARCH) { + if (isBlankG(searchString)) { + // the search string is empty + // leave search results + state = previousState; + // show previous state: bookmark or current group + goto ifState; + } + state = SHOWING_SEARCH; + // SEARCH + smallArrayt *searchR = search_string_in_tree(currentGroup, trimG(&searchString)); + uniqG(searchR, unusedV); + createAllocateSmallArray(hit_tids); + forEachSmallArray(searchR, S) { + castS(s, S); + char *id = ssGet(s); + *(id+ID_LENGTH) = 0; + pushG(hit_tids, id); + finishG(s); + } + smashG(searchR); + uniqG(hit_tids, unusedV); + initiateAllocateSmallArray(&lsGroup); + enumerateSmallArray(hit_tids, H, count) { + castS(h, H); + smallDictt *d = get_task_in_list_group_format(ssGet(h)); + setG(d, "position", count); + pushNFreeG(lsGroup, d); + finishG(h); + } + terminateG(hit_tids); + createAllocateSmallDict(task_d); + setG(task_d, "head", "empty line"); + setG(task_d, "tid", ""); + setG(task_d, "position", 0); + setG(task_d, "group", ""); + setG(task_d, "title", ""); + setG(task_d, "status", ""); + setG(task_d, "ctime", 0); + pushNFreeG(lsGroup, task_d); + } + else { + // show group + lsGroup = list_group(currentGroup); + } + // check the list length and adjust selectedLine when there is only 1 line + if (selectedLine+2 > lenG(lsGroup)) { + selectedLine = lenG(lsGroup)-2; + } +show: + showGroup(); +} + +/** show text in WVIEW window */ +void showSelected(const char *selected) { + if (not currentGroup[0]) { + // path is database list, show nothing + return; + } + if (windows[WVIEW].indexFirstLine < 0) { + // keep first line index in list + windows[WVIEW].indexFirstLine = 0; + } + if (lenG(viewText) <= windows[WVIEW].szrow) { + // the list is smaller than the window + // the list top is the first line in the window + windows[WVIEW].indexFirstLine = 0; + } + //if ((lenG(viewText) > windows[WVIEW].szrow) and ((windows[WVIEW].indexFirstLine + windows[WVIEW].szrow) > lenG(viewText))) { + if checkEndListInWindow(viewText, WVIEW) { + // keep list end at window bottom + windows[WVIEW].indexFirstLine = lenG(viewText) - windows[WVIEW].szrow -1; + } + // in root show all groups + // in groups, show text for task at pos 0 (group title task) + if (viewTextIsGroup and ((selectedLine != 0) or eqG(currentGroup, "root"))) { + // show group at cursor in WGROUP window + enumerateSmallArray(viewText, L, line) { + cast(smallDictt*, d, L) + if (line < windows[WVIEW].indexFirstLine) { + // skip elements before first line on screen + goto endGroup; + } + // print status and title + i8 i; + char *s = statusAndTitle(d, &i); + // set color + if (eqG(getG(d, rtChar, "group"), "GROUP")) { + wcolor_set(subwins[WVIEW], groupListColor, NULL); + } + else { + wcolor_set(subwins[WVIEW], statusColors[i], NULL); + } + mvwaddnstr(subwins[WVIEW], line - windows[WVIEW].indexFirstLine, 0, s, windows[WVIEW].szcol-1); + free(s); +endGroup: + finishG(d); + } + } + else { + // show task text + enumerateSmallArray(viewText, L, line) { + castS(l, L); + if (line < windows[WVIEW].indexFirstLine) { + // skip elements before first line on screen + goto endText; + } + mvwaddnstr(subwins[WVIEW], line - windows[WVIEW].indexFirstLine, 0, ssGet(l), windows[WVIEW].szcol-1); +endText: + finishG(l); + } + } +} + +/** + * generate and display list in WVIEW window + * @param + * selected tid at cursor in WGROUP + */ +void viewSelected(const char *selected) { + if (viewText) { + terminateG(viewText); + } + if (not currentGroup[0]) { + // path is database list, show nothing + return; + } + if (state == SHOWING_BOOKMARKS) { + // setup database, bookmarks can be in any database + save_edi_core_data_location(); + setup_data_location_for_tid(selected); + } + // in root show all groups + // in groups, show text for task at pos 0 (group title task) + if (is_this_task_a_group(selected) and ((selectedLine != 0) or eqG(currentGroup, "root"))) { + // show group + viewText = list_group(selected); + viewTextIsGroup = true; + } + else { + // show task text + viewText = display_task(selected, passThroughTitle); + viewTextIsGroup = false; + } + if (state == SHOWING_BOOKMARKS) { + restore_edi_core_data_location(); + } + showSelected(selected); +} + +void showHelp(void) { + windows[WHELP].subwin = listLength(subwins); +#define margin 4 + windows[WHELP].row = margin; + windows[WHELP].col = margin; + windows[WHELP].szrow = row - windows[WHELP].row - margin; + windows[WHELP].szcol = col - windows[WHELP].col - margin; + void *w; + listPush(&subwins, EVA(w, newwin(windows[WHELP].szrow, windows[WHELP].szcol, windows[WHELP].row, windows[WHELP].col)) ); + setSubwindows(subwins); + wcolor_set(subwins[windows[WHELP].subwin], 1, NULL); + // fill background with color + char *b = malloc(windows[WHELP].szrow * windows[WHELP].szcol +1); + range(i, windows[WHELP].szrow * windows[WHELP].szcol) { + b[i] = ' '; + } + b[windows[WHELP].szrow * windows[WHELP].szcol] = 0; + waddstr(subwins[windows[WHELP].subwin], b); + free(b); + box(w, 0 , 0); + mvwaddnstr(subwins[windows[WHELP].subwin], 0, 2, " HELP ", windows[WHELP].szcol-2); + smallArrayt *help = createSA( + "q quit g set active status", + "INSERT/i new task h set done status", + "DELETE/p delete selected items j set ongoing status", + "z select/deselect all items k set pending status", + "x cut selected items l set inactive status", + "c copy selected items o set unknown status", + "v paste selected items a attach files to cursor item", + "b link selected items d convert task to group or group to task", + "DOWN move cursor down t toggle top/bottom", + "END page down list", + "UP move cursor up", + "HOME page up list", + "RIGHT/ENTER enter group/database (or edit task)", + "LEFT/BACKSPACE browse back in history", + "PAGE DOWN page down description", + "PAGE UP page up description", + "SPACE select multiple items", + "F2 deselect items", + "F3 toggle active filter", + "F4 toggle done filter", + "F5 toggle ongoing filter", + "F6 toggle pending filter", + "F7 toggle inactive filter", + "F8 view all group tasks", + "F9 show task information", + "F10 search", + "F11 add selected items to bookmarks", + "F12 show bookmarks", + "", + "List symbols: 'A' Active, 'D' Done, '>' Ongoing, '^' Pending, '#' inactive, ' ' Unknown" + ); + enumerateSmallArray(help, H, line) { + castS(h, H); + mvwaddnstr(subwins[windows[WHELP].subwin], 2+line, 3, ssGet(h), windows[WHELP].szcol-2); + if (2+line == windows[WHELP].szrow-2) { + // prevent print outside the window + break; + } + finishG(h); + } + terminateG(help); +} + +/** + * first line in main window + * keyboard commands + */ +void topMenu(void) { + // print menu, inverted colors to the end of the screen + int colorPair = 1; + move(0,0); + color_set(colorPair, NULL); + attron(A_REVERSE); + addnstr("F1 Help F2 Deselect ", col-1); + + i32 *flt = (i32 *) getG(status_filters, rtI32P, TASK_STATUS_ACTIVE); + if (*flt == DISABLE) { + color_set(cursorColor, NULL); + } + else { + color_set(colorPair, NULL); + } + + addnstr("F3 Active", col-1); + color_set(colorPair, NULL); + addnstr(" ", col-1); + + flt = (i32 *) getG(status_filters, rtI32P, TASK_STATUS_DONE); + if (*flt == DISABLE) { + color_set(cursorColor, NULL); + } + else { + color_set(colorPair, NULL); + } + + addnstr("F4 Done", col-1); + color_set(colorPair, NULL); + addnstr(" ", col-1); + + flt = (i32 *) getG(status_filters, rtI32P, TASK_STATUS_ONGOING); + if (*flt == DISABLE) { + color_set(cursorColor, NULL); + } + else { + color_set(colorPair, NULL); + } + + addnstr("F5 Ongoing", col-1); + color_set(colorPair, NULL); + addnstr(" ", col-1); + + flt = (i32 *) getG(status_filters, rtI32P, TASK_STATUS_PENDING); + if (*flt == DISABLE) { + color_set(cursorColor, NULL); + } + else { + color_set(colorPair, NULL); + } + + addnstr("F6 Pending", col-1); + color_set(colorPair, NULL); + addnstr(" ", col-1); + + flt = (i32 *) getG(status_filters, rtI32P, TASK_STATUS_INACTIVE); + if (*flt == DISABLE) { + color_set(cursorColor, NULL); + } + else { + color_set(colorPair, NULL); + } + + addnstr("F7 Inactive", col-1); + color_set(colorPair, NULL); + addnstr(" F8 View Group F9 Info F10 Search F11 Bookmark F12 Bookmarks", col-1); + attroff(A_REVERSE); + chgat(-1, A_REVERSE, colorPair , NULL); +} + +/** + * show pathInTree at the bottom of main window + */ +void statusBar(void) { + // print path at bottom + move(row-1,0); + color_set(2, NULL); + smallStringt *p = joinG(pathTitles, "/"); + addnstr(ssGet(p), col-1); + clrtoeol(); +} + +/** + * place WLEFT, WGROUP and WVIEW on screen + * at start and the terminal is resized + */ +void placeCols(void) { + // create columns 15 35 50 + int szWin1 = (col * 3) / (2 * 10); + int szWin2 = (col * 7) / (2 * 10); + int szWin3 = col - szWin1 -szWin2; + + windows[WLEFT].row = 1; + windows[WLEFT].col = 0; + windows[WLEFT].szrow = row-2; + windows[WLEFT].szcol = szWin1-1; + windows[WGROUP].row = 1; + windows[WGROUP].col = szWin1; + windows[WGROUP].szrow = row-2; + windows[WGROUP].szcol = szWin2-1; + windows[WVIEW].row = 1; + windows[WVIEW].col = szWin1+szWin2; + windows[WVIEW].szrow = row-2; + windows[WVIEW].szcol = szWin3; +} + +/** + * create WLEFT, WGROUP and WVIEW + */ +void createCols(void) { + free(subwins); + subwins = NULL; + + placeCols(); + + // initialize firstLineOnScreen index + windows[WLEFT].indexFirstLine = 0; + windows[WGROUP].indexFirstLine = 0; + windows[WVIEW].indexFirstLine = 0; + + // create 3 window colunms WLEFT, WGROUP, WVIEW + WINDOW *w; + + // refresh here, otherwise the new window is invisible + refresh(); + // create left window + w = newwin(windows[WLEFT].szrow, windows[WLEFT].szcol, windows[WLEFT].row, windows[WLEFT].col); + listPush(&subwins, w); + + // create group window + w = newwin(windows[WGROUP].szrow, windows[WGROUP].szcol, windows[WGROUP].row, windows[WGROUP].col); + listPush(&subwins, w); + + // create view window + w = newwin(windows[WVIEW].szrow, windows[WVIEW].szcol, windows[WVIEW].row, windows[WVIEW].col); + listPush(&subwins, w); + + setSubwindows(subwins); +} + +/** + * resize the windows when the terminal is resized + */ +void resizeCols(void) { + + placeCols(); + + // erase previous content to remove all artifacts + eraseSubwindows(); + // refresh to show the windows again + refresh(); + enumerateType(void, subwins, w, i) { + mvwin(*w, windows[i].row, windows[i].col); + wresize(*w, windows[i].szrow, windows[i].szcol); + } +} + +/** + * fill WLEFT, WGROUP, WVIEW + */ +void fillCols(void) { + viewLeftParent(getParentGroupInPath()); + viewGroup(currentGroup); + viewSelected(selectedGroup); +} + +/** + * display again the lists in WLEFT, WGROUP and WVIEW without updates + * when the terminal is resized + */ +void showCols(void) { + showLeft(); + showGroup(); + showSelected(selectedGroup); +} + +/** + * detect when the terminal is resized + */ +void winch_sigaction(int sig, siginfo_t *si, void *arg) { + endwin(); + initscr(); + refresh(); + clear(); + row = LINES; + col = COLS; + topMenu(); + statusBar(); + resizeCols(); + showCols(); + refreshAll(); +} + +void registerSigWinch(void) { + struct sigaction sa; + memset(&sa, 0, sizeof(struct sigaction)); + sigemptyset(&sa.sa_mask); + sa.sa_sigaction = winch_sigaction; + sa.sa_flags = SA_SIGINFO; + sigaction(SIGWINCH, &sa, NULL); +} + +void pasteClipboard(const char *pasteLinkOrCopy) { + // handle multiple databases + save_edi_core_data_location(); + // saved_data_location is destination database + setup_data_location_for_tid(getG(selectedList, rtChar, 0)); + // data_location is source database + bool sameSrcDstDatabase = true; + const char *destination_database_name; + if (not eqG(data_location, saved_data_location)) { + sameSrcDstDatabase = false; + destination_database_name = getDatabaseNameFromPath(saved_data_location); + } + if (paperfunction == COPIED) { + forEachSmallArray(selectedList, T) { + castS(t, T); + if (sameSrcDstDatabase) { + if (eqG(pasteLinkOrCopy, "paste")) { + copy_task_to_a_group(ssGet(t), currentGroup); + } + if (eqG(pasteLinkOrCopy, "link")) { + smallStringt *c; + add_task_reference_to_a_group(ssGet(t), EVA(c, allocG(currentGroup))); + terminateG(c); + } + } + else { + // always copy to different source/destination databases + copy_task_to_database(ssGet(t), destination_database_name, currentGroup); + } + finishG(t); + } + } + if (paperfunction == CUT) { + enumerateSmallArray(selectedList, T, i) { + castS(t, T); + if (sameSrcDstDatabase) { + smallArrayt *a = getG(selectedListAttributes, rtSmallArrayt, i); + if (getG(a, rtI32, 1) == SHOWING_DATABASE) { + move_task_to_a_group(getG(a, rtChar, 0), ssGet(t), currentGroup); + } + else { + // group in selectedListAttributes is invalid + move_task_to_a_group(find_group_containing_task(ssGet(t)), ssGet(t), currentGroup); + } + finishG(a); + } + else { + const char *new_tid = NULL; + smallArrayt *a = getG(selectedListAttributes, rtSmallArrayt, i); + if (getG(a, rtI32, 1) == SHOWING_DATABASE) { + // move to other database + new_tid = move_task_to_a_group_to_database(getG(a, rtChar, 0), ssGet(t), destination_database_name, currentGroup); + } + else { + // group in selectedListAttributes is invalid + new_tid = move_task_to_a_group_to_database(find_group_containing_task(ssGet(t)), ssGet(t), destination_database_name, currentGroup); + } + finishG(a); + + if (new_tid){ + ssize_t index = findG(bookmarks, ssGet(t)); + if (index != -1) { + // update bookmark + setG(bookmarks, index, new_tid); + } + } + } + finishG(t); + } + } + restore_edi_core_data_location(); + // deselect all items + emptyG(selectedList); + emptyG(selectedListAttributes); + paperfunction = NO_PAPER; +} + +void saveEdiState(void) { + char *path = expandHomeG("~/.easydoneit.bin"); + createAllocateSmallJson(ediState); + + createAllocateSmallArray(a); + + range(i, staticArrayCount(pathInTree)) { + createAllocateSmallArray(a2); + pushG(a2, staticArrayGet(pathInTree, i).group); + pushG(a2, staticArrayGet(pathInTree, i).pos); + pushG(a2, staticArrayGet(pathInTree, i).firstLineOnScreen); + pushNFreeG(a, a2); + } + + setNFreeG(ediState, "pathInTree", a); + + setNFreeG(ediState, "pathTitles", pathTitles); + + setG(ediState, "state", (int32_t)state); + setG(ediState, "previousState", (int32_t)previousState); + setG(ediState, "stateInfo", stateInfo); + setG(ediState, "currentGroup", currentGroup); + setG(ediState, "selectedGroup", selectedGroup); + setG(ediState, "selectedLine", selectedLine); + setNFreeG(ediState, "selectedList", selectedList); + setNFreeG(ediState, "selectedListAttributes", selectedListAttributes); + setNFreeG(ediState, "lsGroup", lsGroup); // for search results, avoid crash + // TODO save view text to restore view group + //setNFreeG(ediState, "searchString", searchString); + setG(ediState, "paperfunction", paperfunction); + + //logVarG(ediState); + smallBytest *B = serialG(ediState); + writeFileG(B, path); + terminateManyG(ediState, B); + free(path); +} + +/** true when state is loaded */ +bool loadEdiState(void) { + bool r = false; + char *path = expandHomeG("~/.easydoneit.bin"); + + // use default when edi state is not found + if (fileExists(path)) { + createAllocateSmallBytes(B); + readFileG(B, path); + + createAllocateSmallJson(ediState); + + deserialG(ediState, B); + //logVarG(ediState); + + smallArrayt *a = getG(ediState, rtSmallArrayt, "pathInTree"); + + enumerateSmallArray(a, P, i) { + cast(smallArrayt *, p, P); + staticArrayPush(pathInTree); + strcpy(staticArrayLast(pathInTree).group, getG(p, rtChar, 0)); + staticArrayLast(pathInTree).pos = getG(p, rtI32, 1); + staticArrayLast(pathInTree).firstLineOnScreen = getG(p, rtI32, 2); + finishG(p); + } + + finishG(a); + + pathTitles = getNDupG(ediState, rtSmallArrayt, "pathTitles"); + state = getG(ediState, rtI32, "state"); + previousState = getG(ediState, rtI32, "previousState"); + stateInfo = getG(ediState, rtBool, "stateInfo"); + strcpy(currentGroup, getG(ediState, rtChar, "currentGroup")); + strcpy(selectedGroup, getG(ediState, rtChar, "selectedGroup")); + selectedLine = getG(ediState, rtI32, "selectedLine"); + selectedList = getNDupG(ediState, rtSmallArrayt, "selectedList"); + selectedListAttributes = getNDupG(ediState, rtSmallArrayt,"selectedListAttributes"); + lsGroup = getNDupG(ediState, rtSmallArrayt, "lsGroup"); + //getG(ediState, rtChar, "searchString"); + paperfunction = getG(ediState, rtI32, "paperfunction"); + + terminateManyG(ediState, B); + + r = true; + } + free(path); + return r; +} + + +int main(int argc, char** argv) { + + //char *c = readFileG(c, argv[1]); + //logG(md_highlight(c)); + + initLibsheepy(argv[0]); + + //#if 0 + start("tui"); + + mainwin = initScreen(&row, &col); + + staticArrayInit(pathInTree, MAX_PATH_DEPTH); + + // database top + // path top title is database name + // groupid parameter to addGroupToPath is "", pos is the cursor in the database list + windows[WGROUP].indexFirstLine = 0; + + if (not loadEdiState()) { + initiateAllocateSmallArray(&selectedList); + initiateAllocateSmallArray(&selectedListAttributes); + + addGroupToPath("", 0); + initiateAllocateSmallArray(&pathTitles); + pushG(pathTitles, getG(selected, rtChar, 0)); + strcpy(currentGroup, "root"); + } + + topMenu(); + statusBar(); + createCols(); + registerSigWinch(); + fillCols(); + + refreshAll(); + + int c; + while (((c = wgetch(mainwin)) != 'q') || (state == SEARCH)) { + if (state == HELP) { + state = previousState; + void *w = listPop(&subwins); + setSubwindows(subwins); + werase(mainwin); + //refresh(); + wnoutrefresh(mainwin); + delwin(w); + topMenu(); + statusBar(); + showCols(); + } + else if (state == SEARCH) { + switch(c){ + case KEY_F(1): + state = previousState; + noecho(); + statusBar(); + break; + case '\n': { + // state is SEARCH, viewGroup will perform the search + + move(row-1,0); + color_set(cursorColor, NULL); + clrtoeol(); + addnstr("Searching...", col-1); + chgat(-1, 0, cursorColor, NULL); + refresh(); + + noecho(); + statusBar(); + // scroll WVIEW window to top + windows[WVIEW].indexFirstLine = 0; + eraseSubwindows(); + fillCols(); + break; + } + case KEY_BACKSPACE: + delG(&searchString, -1, 0); + } + if (c < KEY_MIN) { + iInjectS(&searchString, -1, c); + } + if (state == SEARCH) { + move(row-1,0); + color_set(1, NULL); + clrtoeol(); + char *s = appendS("SEARCH (F1 to cancel): ", searchString); + addnstr(s, col-1); + free(s); + chgat(-1, 0, 1, NULL); + } + } + else { + if ((c != KEY_F(9)) and (stateInfo == true)) { + // exit state info when any key is pressed + stateInfo = false; + } + switch(c){ + case 'a': + // TODO add attachments + break; + case 't': + if (eqG(add_top_or_bottom, "top")) { + free(add_top_or_bottom); + add_top_or_bottom = strdup("bottom"); + } + else { + free(add_top_or_bottom); + add_top_or_bottom = strdup("top"); + } + break; + case 'd': + // convert selected items + if (not lenG(selectedList)) { + if (not is_this_task_a_group(selectedGroup)) { + create_group(selectedGroup); + } + else { + convert_group_to_task(selectedGroup); + } + } + else { + // selected items in selectedList + forEachSmallArray(selectedList, T) { + castS(t, T); + if (not is_this_task_a_group(ssGet(t))) { + create_group(ssGet(t)); + } + else { + convert_group_to_task(ssGet(t)); + } + finishG(t); + } + emptyG(selectedList); + emptyG(selectedListAttributes); + } + // scroll WVIEW window to top + windows[WVIEW].indexFirstLine = 0; + eraseSubwindows(); + fillCols(); + break; + case 'i': + case KEY_IC: + // INSERT + if (state == SHOWING_SEARCH) { + // leave search results + state = previousState; + } + create_task(currentGroup); + // refresh screen + system("reset"); + reInitScreen(mainwin); + topMenu(); + statusBar(); + eraseSubwindows(); + // scroll WVIEW window to top + windows[WVIEW].indexFirstLine = 0; + fillCols(); + redrawwin(mainwin); + refresh(); + showCols(); + forEachType(void, subwins, wd) { + redrawwin((WINDOW *)(*wd)); + } + break; + case 'p': + case KEY_DC: + // DELETE + if (state == SHOWING_SEARCH) { + // leave search results + state = previousState; + } + if (not lenG(selectedList)) { + delete_linked_task(currentGroup, selectedGroup); + save_edi_core_data_location(); + if (not tid_exists(selectedGroup)) { + ssize_t index = findG(bookmarks, selectedGroup); + if (index != -1) { + // delete bookmark + delG(bookmarks, index, index+1); + } + } + restore_edi_core_data_location(); + } + else { + // selected items in selectedList + enumerateSmallArray(selectedList, T, i) { + castS(t, T); + smallArrayt *a = getG(selectedListAttributes, rtSmallArrayt, i); + if (getG(a, rtI32, 1) == SHOWING_DATABASE) { + delete_linked_task(getG(a, rtChar, 0), ssGet(t)); + } + else { + // group in selectedListAttributes is invalid + delete_task(ssGet(t)); + } + save_edi_core_data_location(); + if (not tid_exists(ssGet(t))) { + ssize_t index = findG(bookmarks, ssGet(t)); + if (index != -1) { + // delete bookmark + delG(bookmarks, index, index+1); + } + } + restore_edi_core_data_location(); + finishG(a); + finishG(t); + } + emptyG(selectedList); + emptyG(selectedListAttributes); + } + // scroll WVIEW window to top + windows[WVIEW].indexFirstLine = 0; + eraseSubwindows(); + fillCols(); + break; + case ' ': + // select/deselect item under cursor in the group list + if (currentGroup[0]) { + // select only in group, not database list + ssize_t index = findG(selectedList, selectedGroup); + if (index == -1) { + // add new selection + pushG(selectedList, selectedGroup); + createAllocateSmallArray(a); + pushG(a, currentGroup); + pushG(a, (int32_t)state); + pushNFreeG(selectedListAttributes, a); + } + else { + // remove selection + delG(selectedList, index, index+1); + delG(selectedListAttributes, index, index+1); + } + } + case KEY_DOWN: + // move cursor down in WGROUP window + // when the cursor reaches the list bottom + // it goes back to the top + if (selectedLine < lenG(lsGroup)-2) { + // move down the list and stay inside + selectedLine++; + } + else { + selectedLine = 0; + windows[WGROUP].indexFirstLine = 0; + } + if ((selectedLine - windows[WGROUP].indexFirstLine +1) > windows[WGROUP].szrow) { + // scroll to keep cursor on screen + windows[WGROUP].indexFirstLine++; + } + // scroll WVIEW window to top + windows[WVIEW].indexFirstLine = 0; + eraseSubwindows(); + fillCols(); + break; + case KEY_END: + // page down WGROUP window + // when the last page is reached, the cursor is moved to the window bottom + // on next page down, the cursor is moved to the list top + if ((windows[WGROUP].indexFirstLine + windows[WGROUP].szrow) < lenG(lsGroup)-2) { + // list middle is on window bottom + windows[WGROUP].indexFirstLine += windows[WGROUP].szrow; + // keep cursor on window top + if checkEndListInWindow(lsGroup, WGROUP) { + selectedLine = lenG(lsGroup) - windows[WGROUP].szrow -1; + } + else { + selectedLine = windows[WGROUP].indexFirstLine; + } + } + else { + // list end is on window bottom + if (selectedLine != (lenG(lsGroup)-2)) { + // cursor not at window bottom, move cursor to bottom + selectedLine = lenG(lsGroup)-2; + } + else { + // cursor is at bottom, move to list top + selectedLine = 0; + windows[WGROUP].indexFirstLine = 0; + } + } + // scroll WVIEW window to top + windows[WVIEW].indexFirstLine = 0; + eraseSubwindows(); + fillCols(); + break; + case KEY_UP: + // move cursor up in WGROUP window + // when cursor reaches the list top + // it goes back to the bottom + if (selectedLine > 0) { + // keep cursor in list + selectedLine--; + if (selectedLine < windows[WGROUP].indexFirstLine) { + // scroll to keep cursor on screen + windows[WGROUP].indexFirstLine--; + } + } + else { + // cursor is at top, move to list bottom + selectedLine = lenG(lsGroup)-2; + windows[WGROUP].indexFirstLine = lenG(lsGroup) - windows[WGROUP].szrow -1; + } + // scroll WVIEW window to top + windows[WVIEW].indexFirstLine = 0; + eraseSubwindows(); + fillCols(); + break; + case KEY_HOME: + // page up WGROUP window + // when the first page is reached, the cursor is moved to the window top + // on next page up, the cursor is moved to the list bottom + if (((windows[WGROUP].indexFirstLine - windows[WGROUP].szrow)) > 0) { + // list middle is on window top + windows[WGROUP].indexFirstLine -= windows[WGROUP].szrow; + // keep cursor on window bottom + selectedLine = windows[WGROUP].indexFirstLine + windows[WGROUP].szrow -1; + } + else { + // list top is on window top + i32 newIndex = 0; + if (windows[WGROUP].indexFirstLine) { + // currently list top is not in window + if (selectedLine >= windows[WGROUP].szrow) { + // cursor is outside window after page down + // keep cursor at window bottom + selectedLine = windows[WGROUP].szrow -1; + } + } + else { + if (selectedLine != 0) { + // cursor not at window top, move cursor to top + selectedLine = 0; + } + else { + // cursor is at top, move to list bottom + selectedLine = lenG(lsGroup)-2; + newIndex = lenG(lsGroup) - windows[WGROUP].szrow -1; + } + } + // show list top at WGROUP window top + windows[WGROUP].indexFirstLine = newIndex; + } + // scroll WVIEW window to top + windows[WVIEW].indexFirstLine = 0; + eraseSubwindows(); + fillCols(); + break; + case '\n': + case KEY_RIGHT: + // move to selected item (group, database) + if (state == SHOWING_SEARCH) { + // leave search results + state = previousState; + } + // scroll WVIEW window to top + windows[WVIEW].indexFirstLine = 0; + if (state == SHOWING_BOOKMARKS) { + // currently showing bookmarks + // stop showing bookmarks + state = SHOWING_DATABASE; + } + if (!currentGroup[0]) { + // a database is selected + // add empty group id for root in pathInTree + addGroupToPath("", selectedLine); + smallDictt *d = getG(lsGroup, rtSmallDictt, selectedLine); + char *dName = getG(d, rtChar, "title"); + select_database(dName); + + // update database name in status bar + free(popG(pathTitles, rtChar)); + pushG(pathTitles, dName); + + // set current group root, database top + strcpy(currentGroup, "root"); + selectedLine = 0; + statusBar(); + eraseSubwindows(); + fillCols(); + break; + } + // in root enter all groups + // in groups, edit text for task at pos 0 (group title task) + if (is_this_task_a_group(selectedGroup) and ((selectedLine != 0) or eqG(currentGroup, "root"))) { + // a group is selected + // add currentGroup to pathInTree + addGroupToPath(currentGroup, selectedLine); + // set selected group as current group + strcpy(currentGroup, selectedGroup); + // line 0 is group title, select first task in group instead + // viewGroup keeps the cursor in the list + // so when the group is empty, it is moved to line 0 + selectedLine = 1; + + // add new current group title to pathTitles for status bar + pushNFreeG(pathTitles, trimG(get_task_title(currentGroup))); + + statusBar(); + eraseSubwindows(); + fillCols(); + } + else { + // edit task, open editor + edit_task(selectedGroup); + // refresh screen + system("reset"); + reInitScreen(mainwin); + topMenu(); + statusBar(); + eraseSubwindows(); + // scroll WVIEW window to top + windows[WVIEW].indexFirstLine = 0; + fillCols(); + redrawwin(mainwin); + refresh(); + showCols(); + forEachType(void, subwins, wd) { + redrawwin((WINDOW *)(*wd)); + } + } + break; + case KEY_BACKSPACE: + case KEY_LEFT: { + // move back in browsing history + // scroll WVIEW window to top + windows[WVIEW].indexFirstLine = 0; + if (state == SHOWING_SEARCH) { + // leave search results + state = previousState; + eraseSubwindows(); + fillCols(); + break; + } + if (state == SHOWING_BOOKMARKS) { + // currently showing bookmarks + // stop showing bookmarks and stay in same group + state = SHOWING_DATABASE; + eraseSubwindows(); + fillCols(); + break; + } + if ((staticArrayCount(pathInTree) == 1) and (currentGroup[0] not_eq 0)) { + // path is root and currentGroup is not database list + // show database list in WGROUP window + currentGroup[0] = 0; + } + + moveToParentGroup(); + if (lenG(pathTitles) > 1) { + // always keep database name in path title + // to show current database in status bar + free(popG(pathTitles, rtChar)); + } + + statusBar(); + eraseSubwindows(); + fillCols(); + break; + } + case KEY_NPAGE: + // page down WVIEW window + // showSelected keeps the text in the window and adjusts indexFirstLine + windows[WVIEW].indexFirstLine += windows[WVIEW].szrow; + eraseSubwindows(); + /* fillCols(); */ + viewLeftParent(getParentGroupInPath()); + viewGroup(currentGroup); + showSelected(""); + break; + case KEY_PPAGE: + // page up WVIEW window + // showSelected keeps the text in the window and adjusts indexFirstLine + windows[WVIEW].indexFirstLine -= windows[WVIEW].szrow; + eraseSubwindows(); + /* fillCols(); */ + viewLeftParent(getParentGroupInPath()); + viewGroup(currentGroup); + showSelected(""); + break; + case KEY_F(1): + if (state != HELP) { + previousState = state; + state = HELP; + showHelp(); + } + break; + case KEY_F(3): { + // toggle active filter + // deselect all items + emptyG(selectedList); + emptyG(selectedListAttributes); + if (state == SHOWING_SEARCH) { + // leave search results + state = previousState; + } + i32 *flt = (i32 *) getG(status_filters, rtI32P, TASK_STATUS_ACTIVE); + if (*flt == DISABLE) { + *flt = ENABLE; + } + else { + *flt = DISABLE; + } + // scroll WVIEW window to top + windows[WVIEW].indexFirstLine = 0; + eraseSubwindows(); + topMenu(); + fillCols(); + break; + } + case KEY_F(4): { + // toggle done filter + // deselect all items + emptyG(selectedList); + emptyG(selectedListAttributes); + if (state == SHOWING_SEARCH) { + // leave search results + state = previousState; + } + i32 *flt = (i32 *) getG(status_filters, rtI32P, TASK_STATUS_DONE); + if (*flt == DISABLE) { + *flt = ENABLE; + } + else { + *flt = DISABLE; + } + // scroll WVIEW window to top + windows[WVIEW].indexFirstLine = 0; + eraseSubwindows(); + topMenu(); + fillCols(); + break; + } + case KEY_F(5): { + // toggle ongoing filter + // deselect all items + emptyG(selectedList); + emptyG(selectedListAttributes); + if (state == SHOWING_SEARCH) { + // leave search results + state = previousState; + } + i32 *flt = (i32 *) getG(status_filters, rtI32P, TASK_STATUS_ONGOING); + if (*flt == DISABLE) { + *flt = ENABLE; + } + else { + *flt = DISABLE; + } + // scroll WVIEW window to top + windows[WVIEW].indexFirstLine = 0; + eraseSubwindows(); + topMenu(); + fillCols(); + break; + } + case KEY_F(6): { + // toggle pending filter + // deselect all items + emptyG(selectedList); + emptyG(selectedListAttributes); + if (state == SHOWING_SEARCH) { + // leave search results + state = previousState; + } + i32 *flt = (i32 *) getG(status_filters, rtI32P, TASK_STATUS_PENDING); + if (*flt == DISABLE) { + *flt = ENABLE; + } + else { + *flt = DISABLE; + } + // scroll WVIEW window to top + windows[WVIEW].indexFirstLine = 0; + eraseSubwindows(); + topMenu(); + fillCols(); + break; + } + case KEY_F(7): { + // toggle inactive filter + // deselect all items + emptyG(selectedList); + emptyG(selectedListAttributes); + if (state == SHOWING_SEARCH) { + // leave search results + state = previousState; + } + i32 *flt = (i32 *) getG(status_filters, rtI32P, TASK_STATUS_INACTIVE); + if (*flt == DISABLE) { + *flt = ENABLE; + } + else { + *flt = DISABLE; + } + // scroll WVIEW window to top + windows[WVIEW].indexFirstLine = 0; + eraseSubwindows(); + topMenu(); + fillCols(); + break; + } + case KEY_F(8): { + // show all descriptions for tasks in list + if (viewText) { + terminateG(viewText); + } + if (state == SHOWING_SEARCH) { + // show tasks from search results + viewText = listTasksInList(lsGroup); + viewTextIsGroup = true; + } + if (state == SHOWING_DATABASE) { + // show tasks in current group + viewText = listTasksInGroup(currentGroup); + viewTextIsGroup = true; + } + if (state == SHOWING_BOOKMARKS) { + // show tasks from bookmarks + viewText = listTasksInBookmarks(); + viewTextIsGroup = true; + } + // scroll WVIEW window to top + windows[WVIEW].indexFirstLine = 0; + eraseSubwindows(); + /* fillCols(); */ + viewLeftParent(getParentGroupInPath()); + viewGroup(currentGroup); + showSelected(""); + break; + } + case KEY_F(9): + // toggle info + if (not stateInfo) { + stateInfo = true; + if (viewText) { + terminateG(viewText); + } + //viewText = createSA("fwefw", "wfwef"); + viewText = show_group_for_task(selectedGroup); + viewTextIsGroup = false; + // scroll WVIEW window to top + windows[WVIEW].indexFirstLine = 0; + eraseSubwindows(); + /* fillCols(); */ + viewLeftParent(getParentGroupInPath()); + viewGroup(currentGroup); + showSelected(""); + } + else { + stateInfo = false; + // scroll WVIEW window to top + windows[WVIEW].indexFirstLine = 0; + eraseSubwindows(); + fillCols(); + } + break; + case '/': + case KEY_F(10): { + // search string in group + if (state == SHOWING_SEARCH) { + // leave search results + state = previousState; + break; + } + iEmptySF(&searchString); + previousState = state; + state = SEARCH; + echo(); + move(row-1,0); + color_set(1, NULL); + clrtoeol(); + addnstr("SEARCH (F1 to cancel): ", col-1); + chgat(-1, 0, 1, NULL); + break; + } + case KEY_F(11): { + // bookmark items + // remove from bookmark if already bookmarked + if (not lenG(selectedList)) { + ssize_t index = findG(bookmarks, selectedGroup); + if (index == -1) { + // no duplicated bookmarks + // remove empty strings in bookmarks + compactG(bookmarks); + pushG(bookmarks, selectedGroup); + } + else { + // remove bookmark + delG(bookmarks, index, index+1); + } + } + else { + // selected items in selectedList + forEachSmallArray(selectedList, T) { + castS(t, T); + ssize_t index = findG(bookmarks, ssGet(t)); + if (index == -1) { + // no duplicated bookmarks + pushG(bookmarks, ssGet(t)); + } + else { + // remove bookmark + delG(bookmarks, index, index+1); + } + finishG(t); + } + emptyG(selectedList); + emptyG(selectedListAttributes); + } + break; + } + case KEY_F(12): { + // show bookmarks + if (state == SHOWING_SEARCH) { + // leave search results + state = previousState; + } + if ((state != SHOWING_BOOKMARKS) and (not eqG(getG(bookmarks, rtChar, 0), ""))) { + state = SHOWING_BOOKMARKS; + } + else if (state == SHOWING_BOOKMARKS) { + state = SHOWING_DATABASE; + } + // scroll WVIEW window to top + windows[WVIEW].indexFirstLine = 0; + eraseSubwindows(); + fillCols(); + break; + } + case 'g': + // set selected item active + if (currentGroup[0]) { + // change status for all selected tasks + if (not lenG(selectedList)) { + set_status(selectedGroup, TASK_STATUS_ACTIVE); + } + else { + // selected items in selectedList + forEachSmallArray(selectedList, T) { + castS(t, T); + set_status(ssGet(t), TASK_STATUS_ACTIVE); + finishG(t); + } + emptyG(selectedList); + emptyG(selectedListAttributes); + } + + // scroll WVIEW window to top + windows[WVIEW].indexFirstLine = 0; + eraseSubwindows(); + fillCols(); + } + break; + case 'h': + // set selected item active + if (currentGroup[0]) { + // change status for all selected tasks + if (not lenG(selectedList)) { + set_status(selectedGroup, TASK_STATUS_DONE); + } + else { + // selected items in selectedList + forEachSmallArray(selectedList, T) { + castS(t, T); + set_status(ssGet(t), TASK_STATUS_DONE); + finishG(t); + } + emptyG(selectedList); + emptyG(selectedListAttributes); + } + + // scroll WVIEW window to top + windows[WVIEW].indexFirstLine = 0; + eraseSubwindows(); + fillCols(); + } + break; + case 'j': + // set selected item active + if (currentGroup[0]) { + // change status for all selected tasks + if (not lenG(selectedList)) { + set_status(selectedGroup, TASK_STATUS_ONGOING); + } + else { + // selected items in selectedList + forEachSmallArray(selectedList, T) { + castS(t, T); + set_status(ssGet(t), TASK_STATUS_ONGOING); + finishG(t); + } + emptyG(selectedList); + emptyG(selectedListAttributes); + } + + // scroll WVIEW window to top + windows[WVIEW].indexFirstLine = 0; + eraseSubwindows(); + fillCols(); + } + break; + case 'k': + // set selected item active + if (currentGroup[0]) { + // change status for all selected tasks + if (not lenG(selectedList)) { + set_status(selectedGroup, TASK_STATUS_PENDING); + } + else { + // selected items in selectedList + forEachSmallArray(selectedList, T) { + castS(t, T); + set_status(ssGet(t), TASK_STATUS_PENDING); + finishG(t); + } + emptyG(selectedList); + emptyG(selectedListAttributes); + } + + // scroll WVIEW window to top + windows[WVIEW].indexFirstLine = 0; + eraseSubwindows(); + fillCols(); + } + break; + case 'l': + // set selected item active + if (currentGroup[0]) { + // change status for all selected tasks + if (not lenG(selectedList)) { + set_status(selectedGroup, TASK_STATUS_INACTIVE); + } + else { + // selected items in selectedList + forEachSmallArray(selectedList, T) { + castS(t, T); + set_status(ssGet(t), TASK_STATUS_INACTIVE); + finishG(t); + } + emptyG(selectedList); + emptyG(selectedListAttributes); + } + + // scroll WVIEW window to top + windows[WVIEW].indexFirstLine = 0; + eraseSubwindows(); + fillCols(); + } + break; + case 'o': + // set selected item active + if (currentGroup[0]) { + // change status for all selected tasks + if (not lenG(selectedList)) { + set_status(selectedGroup, TASK_STATUS_VOID); + } + else { + // selected items in selectedList + forEachSmallArray(selectedList, T) { + castS(t, T); + set_status(ssGet(t), TASK_STATUS_VOID); + finishG(t); + } + emptyG(selectedList); + emptyG(selectedListAttributes); + } + + // scroll WVIEW window to top + windows[WVIEW].indexFirstLine = 0; + eraseSubwindows(); + fillCols(); + } + break; + case 'z': + // select all in list + if (currentGroup[0]) { + // select only in group, not database list + if (lenG(selectedList)) { + // deselect all items + emptyG(selectedList); + emptyG(selectedListAttributes); + } + else { + // nothing is selected, select all + // except group title + enumerateSmallArray(lsGroup, D, i) { + cast(smallDictt*, d, D); + // in groups don't select group title + if ((i not_eq 0) or eqG(currentGroup, "root") or (state != SHOWING_DATABASE)) { + char *s = getG(d, rtChar, "tid"); + if (not isBlankG(s)) { + pushG(selectedList, s); + createAllocateSmallArray(a); + pushG(a, currentGroup); + pushG(a, (int32_t)state); + pushNFreeG(selectedListAttributes, a); + } + } + finishG(d); + } + } + showCols(); + } + break; + case 'x': + // cut selected items + if (currentGroup[0]) { + // select only in group, not database list + if (not lenG(selectedList)) { + pushG(selectedList, selectedGroup); + createAllocateSmallArray(a); + pushG(a, currentGroup); + pushG(a, (int32_t)state); + pushNFreeG(selectedListAttributes, a); + } + paperfunction = CUT; + + move(row-1,0); + color_set(cursorColor, NULL); + clrtoeol(); + addnstr("Cut selected items, move to destination and paste (v)", col-1); + chgat(-1, 0, cursorColor, NULL); + } + break; + case 'c': + // copy selected items + if (currentGroup[0]) { + // select only in group, not database list + if (not lenG(selectedList)) { + pushG(selectedList, selectedGroup); + createAllocateSmallArray(a); + pushG(a, currentGroup); + pushG(a, (int32_t)state); + pushNFreeG(selectedListAttributes, a); + } + paperfunction = COPIED; + + move(row-1,0); + color_set(cursorColor, NULL); + clrtoeol(); + addnstr("Copied selected items, move to destination and paste (v) or link (b)", col-1); + chgat(-1, 0, cursorColor, NULL); + } + break; + case 'v': + if ((currentGroup[0]) and (state == SHOWING_DATABASE) and (paperfunction != NO_PAPER)) { + pasteClipboard("paste"); + // scroll WVIEW window to top + windows[WVIEW].indexFirstLine = 0; + eraseSubwindows(); + fillCols(); + } + break; + case 'b': + // link selected tasks + if ((currentGroup[0]) and (state == SHOWING_DATABASE) and (paperfunction != NO_PAPER)) { + pasteClipboard("link"); + // scroll WVIEW window to top + windows[WVIEW].indexFirstLine = 0; + eraseSubwindows(); + fillCols(); + } + break; + /* case KEY_MOUSE: */ + /* mvaddstr(row-4, 0, "mouse event"); */ + /* if(getmouse(&event) == OK) { */ + /* mvprintw(row-3,0,"x %d y %d z %d bstate 0x%x", event.x, event.y, event.z, event.bstate); */ + /* } */ + /* break; */ + } + } + refreshAll(); + } +exit: + finalizeScreen(mainwin); + + //test(); + + // save bookmarks in ini file + smallDictt *data = getG(ini, rtSmallDictt, "data"); + smallDictt *locations = getG(ini, rtSmallDictt, "locations"); + smallDictt *filters = getG(ini, rtSmallDictt, "filters"); + smallDictt *desktop = getG(ini, rtSmallDictt, "desktop"); + + setNFreeG(data, "location", data_location); + + setNFreeG(locations, "add_top_or_bottom", add_top_or_bottom); + + enumerateS(TASK_STATUS_TRIM, name, i) { + i32 flt = getG(status_filters, rtI32, i); + setG(filters, name, STATUS_FILTER_STATES[flt]); + } + if (desktop) { + smallStringt *b = joinG(bookmarks, "|"); + setNFreeG(desktop, "bookmarks", b); + } + else { + // no desktop section in ini file, empty bookmarks + if (lenG(bookmarks)) { + // create a desktop section + desktop = allocG(rtSmallDictt); + smallStringt *b = joinG(bookmarks, "|"); + setNFreeG(desktop, "bookmarks", b); + setG(ini, "desktop", desktop); + } + } + + char *p = expandHomeG("~/.easydoneit.ini"); + saveIni(ini, p); + free(p); + //logVarG(ini); + + saveEdiState(); + + terminateG(ini); + + finalizeLibsheepy(); + + XSUCCESS +} diff --git a/package.yml b/package.yml @@ -0,0 +1,20 @@ +--- + name: easydoneitCTui + version: 0.0.19 + description: Easydoneit TUI + bin: edt.c + lflags: -lncurses + repository: + type: git + url: git+https://github.com/RemyNoulin/easydoneitCTui.git + keywords: + - utility + - command + author: Remy Noulin + license: MIT + bugs: + url: https://github.com/RemyNoulin/easydoneitCTui/issues + homepage: https://github.com/RemyNoulin/easydoneitCTui + dependencies: + ini: "" + md: "" diff --git a/root.txt b/root.txt @@ -0,0 +1 @@ +First Level diff --git a/tui/package.yml b/tui/package.yml @@ -0,0 +1,16 @@ +--- + name: tui + version: 0.0.1 + description: "Text User Interface library" + bin: ./tui.c + repository: + type: git + url: git+https://github.com/USER/tui.git + keywords: + - library + - command + author: Remy Noulin + license: MIT + bugs: + url: https://github.com/USER/tui/issues + homepage: https://github.com/USER/tui#readme diff --git a/tui/tui.c b/tui/tui.c @@ -0,0 +1,92 @@ +#include "libsheepyObject.h" +#include <ncurses.h> + +WINDOW *initScreen(int *row, int *col) { + WINDOW *w; + + w = initscr(); + pTestErrorCmd(w == NULL, XFAILURE); + + getmaxyx(stdscr,*row,*col); + + // prevent ctrl-c: + raw(); + //cbreak(); + // dont echo what is typed + noecho(); + // hide cursor + curs_set(0); + + if ( has_colors() ) { + start_color(); + /* Initialize a bunch of colour pairs, where: + init_pair(pair number, foreground, background); + specifies the pair. */ + + init_pair(1, COLOR_BLUE, COLOR_WHITE); + init_pair(2, COLOR_GREEN, COLOR_BLACK); + init_pair(3, COLOR_YELLOW, COLOR_BLACK); + init_pair(4, COLOR_BLUE, COLOR_BLACK); + init_pair(5, COLOR_MAGENTA, COLOR_BLACK); + init_pair(6, COLOR_CYAN, COLOR_BLACK); + init_pair(7, COLOR_BLUE, COLOR_WHITE); + init_pair(8, COLOR_WHITE, COLOR_RED); + init_pair(9, COLOR_BLACK, COLOR_GREEN); + init_pair(10, COLOR_BLUE, COLOR_YELLOW); + init_pair(11, COLOR_WHITE, COLOR_BLUE); + init_pair(12, COLOR_WHITE, COLOR_MAGENTA); + init_pair(13, COLOR_BLACK, COLOR_CYAN); + init_pair(14, COLOR_RED, COLOR_BLACK); + } + + /* Get all the mouse events */ + // disable mouse to have copy/paste mousemask(ALL_MOUSE_EVENTS, NULL); + + // Enable F keys + keypad(w, 1); + + return w; +} + +void reInitScreen(WINDOW *w) { + // prevent ctrl-c: + raw(); + //cbreak(); + // dont echo what is typed + noecho(); + // hide cursor + curs_set(1); + curs_set(0); + + /* Get all the mouse events */ + // disable mouse to have copy/paste mousemask(ALL_MOUSE_EVENTS, NULL); + + // Enable F keys + keypad(w, 1); +} + +void finalizeScreen(WINDOW *w) { + delwin(w); + endwin(); + refresh(); +} + +static void **subWins = NULL; + +void setSubwindows(void **subwins) { + subWins = subwins; +} + +void refreshAll(void) { + forEachType(void, subWins, wd) { + wnoutrefresh(*wd); + } + doupdate(); + //refresh(); +} + +void eraseSubwindows(void) { + forEachType(void, subWins, wd) { + werase(*wd); + } +} diff --git a/tui/tui.h b/tui/tui.h @@ -0,0 +1,7 @@ + +WINDOW *initScreen(int *row, int *col); +void reInitScreen(WINDOW *w); +void finalizeScreen(WINDOW *w); +void setSubwindows(void **subwins); +void refreshAll(void); +void eraseSubwindows(void);