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:
| A | edCore.c | | | 4531 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | edCore.h | | | 71 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | edt.c | | | 1996 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | package.yml | | | 20 | ++++++++++++++++++++ |
| A | root.txt | | | 1 | + |
| A | tui/package.yml | | | 16 | ++++++++++++++++ |
| A | tui/tui.c | | | 92 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | tui/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"> </font></td> */
+/* #</tr> */
+/* #:define read_description */
+/* # convert < to < and > > to ignore html tags in title line */
+/* s = s.replace('<','<').replace('>','>') */
+/* f = open generate_task_path(tid)+os.sep+'description.txt' */
+/* # remove title line, convert < to < and > > to ignore html tags */
+/* description_l = ['%s<br>'%i rstrip.replace('<','<').replace('>','>') 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"> </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);