525 lines
8.9 KiB
C
525 lines
8.9 KiB
C
/******************************************************************************
|
|
*
|
|
* Copyright (c) 2017-2019 by Löwenware Ltd
|
|
* Please, refer LICENSE file for legal information
|
|
*
|
|
******************************************************************************/
|
|
|
|
/**
|
|
* @file todo.c
|
|
* @author Ilja Kartašov <ik@lowenware.com>
|
|
* @brief
|
|
*
|
|
* @see https://lowenware.com/
|
|
*/
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <uuid.h>
|
|
#include <errno.h>
|
|
#include <cStuff/string.h>
|
|
#include <cStuff/file.h>
|
|
#include <components/log.h>
|
|
#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);
|
|
}
|