/****************************************************************************** * * Copyright (c) 2017-2019 by Löwenware Ltd * Please, refer LICENSE file for legal information * ******************************************************************************/ /** * @file todo.c * @author Ilja Kartašov * @brief * * @see https://lowenware.com/ */ #include #include #include #include #include #include #include #include "todo.h" AislStatus ax_todo_init(AxTodo self, const char *file) { uuid_generate(self->list_id); self->file = file; if (cstuff_sprintf(&self->swap, "%s.swap", file) == -1) return AISL_MALLOC_ERROR; return AISL_SUCCESS; } void ax_todo_release(AxTodo self) { if (self->swap) free(self->swap); } static AislStatus ax_todo_add_project(CStuffList list, time_t completed, char *key, size_t k_len) { int i; AxTodoProject proj; for (i = 0; i < list->length; i++) { proj = list->items[i]; if (strncmp(proj->name, key, k_len) == 0 && !proj->name[k_len]) { proj->ref_count++; if (proj->last_completed < completed) { proj->last_completed = completed; } return true; } } if (!(proj = calloc(1, sizeof (*proj)))) return false; if (cstuff_strncpy(&proj->name, key, k_len) != -1) { proj->ref_count = 1; proj->last_completed = completed; uuid_generate(proj->project_id); if (cstuff_list_append(list, proj) != -1) return true; } ax_todo_project_free(proj); return false; } static AislStatus ax_todo_add_tag(CStuffList list, char *key, size_t k_len) { int i; AxTodoTag tag; for (i = 0; i < list->length; i++) { tag = list->items[i]; if (strncmp(tag->name, key, k_len) == 0 && !tag->name[k_len]) { tag->ref_count++; return true; } } if (!(tag = calloc(1, sizeof (*tag)))) return false; if (cstuff_strncpy(&tag->name, key, k_len) != -1) { tag->ref_count = 1; uuid_generate(tag->tag_id); if (cstuff_list_append(list, tag) != -1) return true; } ax_todo_tag_free(tag); return false; } static AislStatus ax_todo_read_keys(AxTodoTask task, CStuffList projs, CStuffList tags) { const char tokens[] = "+@"; char *s = task->description; size_t offset = 0; for (;;) { char t; offset = strcspn(s, tokens); s += offset; if ((t = *(s++)) == 0) break; offset = strcspn(s, " "); if (offset) { if ((t == '+' && !ax_todo_add_project(projs, task->completed, s, offset)) || (t == '@' && !ax_todo_add_tag(tags, s, offset))) { return AISL_MALLOC_ERROR; } s += offset; } } return AISL_SUCCESS; } AislStatus ax_todo_read(AxTodo self, CStuffList tasks, CStuffList projs, CStuffList tags) { FILE *f = NULL; size_t sz = 256; char *buf; ssize_t rs; AislStatus result = AISL_SUCCESS; if (!(buf = malloc(sz))) return AISL_MALLOC_ERROR; if (!(f = fopen(self->file, "r"))) goto e_syscall; while ((rs = getdelim(&buf, &sz, '\n', f)) != -1) { AxTodoTask task; if (!(task = ax_todo_task_new(self->list_id))) goto e_malloc; if (ax_todo_task_set(task, buf, rs) != 0) { ax_todo_task_free(task); goto e_input; } if (cstuff_list_append(tasks, task) == -1) { ax_todo_task_free(task); goto e_malloc; } if ((result = ax_todo_read_keys(task, projs, tags)) != AISL_SUCCESS) { goto e_malloc; } } switch(errno) { case 0: goto finally; case ENOMEM: goto e_malloc; default: goto e_syscall; } e_input: result = AISL_INPUT_ERROR; goto finally; e_malloc: result = AISL_MALLOC_ERROR; goto finally; e_syscall: result = AISL_SYSCALL_ERROR; finally: free(buf); if (f) fclose(f); return result; } static void ax_todo__write_dt(FILE *f, time_t dt) { char str_dt[16]; struct tm *p_stm = gmtime(&dt); (void) strftime(str_dt, sizeof (str_dt), "%Y-%m-%d", p_stm); fprintf(f, "%s ", str_dt); } AislStatus ax_todo_write(AxTodo self, CStuffList list) { FILE *f; int i; if (!(f = fopen(self->swap, "w"))) return AISL_SYSCALL_ERROR; for (i = 0; i < list->length; i++) { AxTodoTask task = (AxTodoTask) list->items[i]; if (uuid_compare(task->list_id, self->list_id) != 0) continue; if (task->is_done) { fwrite("x ", 1, 2, f); } if (task->priority && task->priority != AX_TODO_ZERO_PRIORITY) { fprintf(f, "(%c) ", task->priority); } if (task->is_done) { if (task->completed) { ax_todo__write_dt(f, task->completed); if (task->created) { ax_todo__write_dt(f, task->created); } } } else if (task->created) { ax_todo__write_dt(f, task->created); } fprintf(f, "%s\n", task->description); } fclose(f); if (cstuff_file_move(self->swap, self->file) != 0) return AISL_SYSCALL_ERROR; return AISL_SUCCESS; } AxTodoTask ax_todo_task_new(uuid_t list_id) { AxTodoTask result; if ((result = calloc(1, sizeof (*result))) != NULL) { uuid_copy(result->list_id, list_id); uuid_generate(result->task_id); result->priority = AX_TODO_ZERO_PRIORITY; } return result; } void ax_todo_task_release(AxTodoTask task) { if (task->description) free(task->description); } void ax_todo_task_free(AxTodoTask task) { ax_todo_task_release(task); free(task); } static int ax_todo__get_priority(char *buf, char *p_priority) { int result; char priority = 0; const char fmt[] = {'(', 'A', ')', ' '}; for (result = 0; result < 4; result++) { switch (result) { case 0: case 2: case 3: if (buf[result] == fmt[result]) continue; break; case 1: priority = buf[result]; if (!(priority < 0x41 || priority > 0x5A)) continue; break; } return 0; } *p_priority = priority; return result; } int ax_todo__get_dt(char *buf, time_t *p_dt) { struct tm stm = {0}; int result, *p_num = NULL, b_hour = 0, b_min = 0, usec = 0, z_bias = 0; const char fmt[] = "Y-M-DTH:m:s.uZB:b ", *f = fmt; char *ptr = NULL; setenv("TZ", "UTC", 1); for (result = 0; *f != 0; result++, f++) { switch(*f) { case 'Y': p_num = &stm.tm_year; break; case 'M': p_num = &stm.tm_mon; break; case 'D': p_num = &stm.tm_mday; break; case 'H': p_num = &stm.tm_hour; break; case 'm': p_num = &stm.tm_min; break; case 's': p_num = &stm.tm_sec; break; case 'u': p_num = &usec; break; case 'B': p_num = &b_hour; break; case 'b': p_num = &b_min; break; case '.': if (buf[result] != *f) { f++; } continue; case 'T': if (buf[result] == *f) continue; if (buf[result] == ' ') { f = &fmt[17]; continue; } goto e_input; case 'Z': if (buf[result] == *f) { f += 3; z_bias = 'Z'; } else if (buf[result] == '-') { z_bias = -1; } else if (buf[result] == '+') { z_bias = 1; } else { goto e_input; } continue; case '-': case ':': case ' ': if (buf[result] == *f) continue; /* go through */ default: goto e_input; } /* switch */ *p_num = strtol(&buf[result], &ptr, 10); if (!ptr || ptr == &buf[result]) { if (f > &fmt[6] && buf[result] == ' ') { f = &fmt[17]; /* skip optional part of timestamp */ continue; } goto e_input; } result += (ptr - &buf[result] - 1); } /* for */ /* validate results */ /* ToDo: improve (stm.tm_mday < 31) check */ if (!(stm.tm_year < 1900) && (stm.tm_mon) && (stm.tm_mday) && (stm.tm_mon < 13) && (stm.tm_mday < 31) && (stm.tm_hour < 24) && (stm.tm_min < 60) && (stm.tm_sec < 60)) { stm.tm_year -= 1900; stm.tm_mon--; if (z_bias == 0) { /* local time zone */ *p_dt = mktime(&stm); return result; } if (z_bias != 'Z') { stm.tm_hour += (b_hour * z_bias); stm.tm_min += (b_min * z_bias); } *p_dt = mktime(&stm); unsetenv("TZ"); return result; } e_input: /* AX_LOG_DEBUG("Parse error: %c(%d) at %d", *f, (int) (f - fmt), result); */ return 0; (void) usec; } void ax_todo__remove_line_break(char *description) { int move = 0; char c; do { c = *description; if (c == '\r' || c == '\n') { move++; continue; } *(description - move) = c; } while (c != '\0'); } AislStatus ax_todo_task_set(AxTodoTask task, char *buf, ssize_t length) { bool is_done; int l; if (strncmp(buf, "x ", 2) == 0) { is_done = true; buf += 2; } else { is_done = false; } task->is_done = is_done; if ((l = ax_todo__get_priority(buf, &task->priority)) != 0) { buf += l; } l = ax_todo__get_dt(buf, (is_done) ? &task->completed : &task->created); if (l != 0) { buf += l; if (is_done && (l = ax_todo__get_dt(buf, &task->created)) != 0) { buf += l; } } if ((l = cstuff_strcpy(&task->description, buf)) == -1) { return AISL_MALLOC_ERROR; } if (l > 1 && task->description[l-1] == '\n') task->description[l-1] = 0; return AISL_SUCCESS; } void ax_todo_project_free(AxTodoProject project) { if (project->name) free(project->name); free(project); } void ax_todo_tag_free(AxTodoTag tag) { if (tag->name) free(tag->name); free(tag); }