M Makefile => Makefile +1 -1
@@ 3,7 3,7 @@
include config.mk
-SRC = stask.c data.c gui.c
+SRC = stask.c data.c doc.c gui.c
OBJ = ${SRC:.c=.o}
all: stask
M config.mk => config.mk +1 -1
@@ 1,7 1,7 @@
# stask - simple task manager
# See LICENSE file for copyright and license details.
-VERSION = 0.1
+VERSION = 0.2
PREFIX = /usr/local
MANPREFIX = ${PREFIX}/share/man
M data.c => data.c +78 -0
@@ 55,6 55,8 @@ db_open(TaskDB *tdb)
snprintf(tdb->dbpath, sizeof(tdb->dbpath),
"%s/stask.db", dir);
+ snprintf(tdb->docdir, sizeof(tdb->docdir),
+ "%s", dir);
rc = sqlite3_open(tdb->dbpath, &tdb->db);
if (rc != SQLITE_OK) {
@@ 385,3 387,79 @@ task_count(TaskDB *tdb, int list_id, int *total, int *done)
sqlite3_finalize(stmt);
return 0;
}
+
+int
+task_get(TaskDB *tdb, int task_id, Task *out)
+{
+ sqlite3_stmt *stmt;
+ int rc;
+
+ rc = sqlite3_prepare_v2(tdb->db,
+ "SELECT id, list_id, text, done, added "
+ "FROM tasks WHERE id = ?;",
+ -1, &stmt, NULL);
+ if (rc != SQLITE_OK)
+ return -1;
+
+ sqlite3_bind_int(stmt, 1, task_id);
+ rc = sqlite3_step(stmt);
+ if (rc != SQLITE_ROW) {
+ sqlite3_finalize(stmt);
+ return -1;
+ }
+
+ out->id = sqlite3_column_int(stmt, 0);
+ out->list_id = sqlite3_column_int(stmt, 1);
+ snprintf(out->text, sizeof(out->text),
+ "%s", sqlite3_column_text(stmt, 2));
+ out->done = sqlite3_column_int(stmt, 3);
+ snprintf(out->added, sizeof(out->added),
+ "%s", sqlite3_column_text(stmt, 4));
+ sqlite3_finalize(stmt);
+ return 0;
+}
+
+int
+task_update_text(TaskDB *tdb, int task_id, const char *text)
+{
+ sqlite3_stmt *stmt;
+ int rc;
+
+ rc = sqlite3_prepare_v2(tdb->db,
+ "UPDATE tasks SET text = ? WHERE id = ?;",
+ -1, &stmt, NULL);
+ if (rc != SQLITE_OK)
+ return -1;
+
+ sqlite3_bind_text(stmt, 1, text, -1, SQLITE_STATIC);
+ sqlite3_bind_int(stmt, 2, task_id);
+ rc = sqlite3_step(stmt);
+ sqlite3_finalize(stmt);
+ return rc == SQLITE_DONE ? 0 : -1;
+}
+
+char *
+task_list_name(TaskDB *tdb, int list_id)
+{
+ sqlite3_stmt *stmt;
+ int rc;
+ static char name[128];
+
+ rc = sqlite3_prepare_v2(tdb->db,
+ "SELECT name FROM lists WHERE id = ?;",
+ -1, &stmt, NULL);
+ if (rc != SQLITE_OK)
+ return NULL;
+
+ sqlite3_bind_int(stmt, 1, list_id);
+ rc = sqlite3_step(stmt);
+ if (rc != SQLITE_ROW) {
+ sqlite3_finalize(stmt);
+ return NULL;
+ }
+
+ snprintf(name, sizeof(name),
+ "%s", sqlite3_column_text(stmt, 0));
+ sqlite3_finalize(stmt);
+ return name;
+}
A doc.c => doc.c +324 -0
@@ 0,0 1,324 @@
+/* stask - simple task manager
+ * See LICENSE file for copyright and license details. */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "stask.h"
+
+static void
+ensure_dir(const char *path)
+{
+ char tmp[1024];
+ char *p;
+
+ snprintf(tmp, sizeof(tmp), "%s", path);
+ for (p = tmp + 1; *p; p++) {
+ if (*p == '/') {
+ *p = '\0';
+ mkdir(tmp, 0755);
+ *p = '/';
+ }
+ }
+ mkdir(tmp, 0755);
+}
+
+void
+doc_dir(TaskDB *tdb, const char *listname, char *buf, int len)
+{
+ snprintf(buf, len, "%s/docs/%s", tdb->docdir, listname);
+}
+
+void
+doc_path(TaskDB *tdb, const char *listname, int task_id,
+ char *buf, int len)
+{
+ snprintf(buf, len, "%s/docs/%s/%d.md",
+ tdb->docdir, listname, task_id);
+}
+
+int
+doc_create(TaskDB *tdb, const char *listname,
+ int task_id, const char *title, const char *date)
+{
+ char path[1024], dir[1024];
+ FILE *fp;
+
+ doc_dir(tdb, listname, dir, sizeof(dir));
+ ensure_dir(dir);
+
+ doc_path(tdb, listname, task_id, path, sizeof(path));
+ fp = fopen(path, "w");
+ if (!fp) {
+ fprintf(stderr, "stask: cannot create %s\n", path);
+ return -1;
+ }
+
+ fprintf(fp, "---\n");
+ fprintf(fp, "title: \"%s\"\n", title);
+ fprintf(fp, "date: %s\n", date);
+ fprintf(fp, "done: false\n");
+ fprintf(fp, "---\n\n");
+
+ fclose(fp);
+ return 0;
+}
+
+int
+doc_delete(TaskDB *tdb, const char *listname, int task_id)
+{
+ char path[1024];
+
+ doc_path(tdb, listname, task_id, path, sizeof(path));
+ return unlink(path);
+}
+
+int
+doc_delete_all(TaskDB *tdb, const char *listname)
+{
+ Task *tasks;
+ int i, n;
+ List list;
+ char dir[1024];
+
+ if (list_by_name(tdb, listname, &list) < 0)
+ return -1;
+
+ if (task_all(tdb, list.id, &tasks, &n) == 0) {
+ for (i = 0; i < n; i++)
+ doc_delete(tdb, listname, tasks[i].id);
+ free(tasks);
+ }
+
+ doc_dir(tdb, listname, dir, sizeof(dir));
+ rmdir(dir); /* only succeeds if empty */
+ return 0;
+}
+
+int
+doc_edit(TaskDB *tdb, const char *listname, int task_id)
+{
+ char path[1024];
+ const char *editor;
+ char cmd[2048];
+ int ret;
+
+ doc_path(tdb, listname, task_id, path, sizeof(path));
+
+ if (access(path, F_OK) != 0) {
+ fprintf(stderr,
+ "stask: no doc for task %d\n", task_id);
+ return -1;
+ }
+
+ editor = getenv("EDITOR");
+ if (!editor)
+ editor = "vi";
+
+ snprintf(cmd, sizeof(cmd), "%s '%s'", editor, path);
+ ret = system(cmd);
+
+ if (ret == 0)
+ doc_sync_title(tdb, listname, task_id);
+
+ return ret;
+}
+
+int
+doc_show(TaskDB *tdb, const char *listname, int task_id)
+{
+ char path[1024];
+ FILE *fp;
+ char line[1024];
+
+ doc_path(tdb, listname, task_id, path, sizeof(path));
+
+ fp = fopen(path, "r");
+ if (!fp) {
+ fprintf(stderr,
+ "stask: no doc for task %d\n", task_id);
+ return -1;
+ }
+
+ while (fgets(line, sizeof(line), fp))
+ fputs(line, stdout);
+
+ fclose(fp);
+ return 0;
+}
+
+/* parse title from yaml frontmatter */
+static int
+parse_title(const char *path, char *title, int len)
+{
+ FILE *fp;
+ char line[1024];
+ int in_fm = 0;
+ char *p, *end;
+
+ fp = fopen(path, "r");
+ if (!fp)
+ return -1;
+
+ while (fgets(line, sizeof(line), fp)) {
+ /* trim trailing newline */
+ p = line + strlen(line) - 1;
+ while (p >= line && (*p == '\n' || *p == '\r'))
+ *p-- = '\0';
+
+ if (strcmp(line, "---") == 0) {
+ if (!in_fm) {
+ in_fm = 1;
+ continue;
+ } else {
+ break; /* end of frontmatter */
+ }
+ }
+
+ if (!in_fm)
+ continue;
+
+ if (strncmp(line, "title:", 6) == 0) {
+ p = line + 6;
+ while (*p == ' ' || *p == '\t')
+ p++;
+ /* strip quotes */
+ if (*p == '"' || *p == '\'') {
+ p++;
+ end = p + strlen(p) - 1;
+ if (end > p && (*end == '"' || *end == '\''))
+ *end = '\0';
+ }
+ snprintf(title, len, "%s", p);
+ fclose(fp);
+ return 0;
+ }
+ }
+
+ fclose(fp);
+ return -1;
+}
+
+/* update done field in frontmatter */
+static int
+update_fm_field(const char *path, const char *field,
+ const char *value)
+{
+ FILE *fp;
+ char *buf;
+ long fsize;
+ char line[1024], out[65536];
+ int in_fm = 0, found = 0, olen = 0;
+ char *p, *src;
+
+ fp = fopen(path, "r");
+ if (!fp)
+ return -1;
+
+ fseek(fp, 0, SEEK_END);
+ fsize = ftell(fp);
+ fseek(fp, 0, SEEK_SET);
+
+ buf = malloc(fsize + 1);
+ if (!buf) {
+ fclose(fp);
+ return -1;
+ }
+ fsize = fread(buf, 1, fsize, fp);
+ buf[fsize] = '\0';
+ fclose(fp);
+
+ src = buf;
+ out[0] = '\0';
+
+ while (*src) {
+ /* read a line */
+ p = strchr(src, '\n');
+ if (p) {
+ int llen = (int)(p - src);
+ if (llen >= (int)sizeof(line) - 1)
+ llen = sizeof(line) - 2;
+ memcpy(line, src, llen);
+ line[llen] = '\0';
+ src = p + 1;
+ } else {
+ snprintf(line, sizeof(line), "%s", src);
+ src += strlen(src);
+ }
+
+ if (strcmp(line, "---") == 0) {
+ if (!in_fm) {
+ in_fm = 1;
+ } else {
+ in_fm = 0;
+ }
+ olen += snprintf(out + olen,
+ sizeof(out) - olen, "%s\n", line);
+ continue;
+ }
+
+ if (in_fm && strncmp(line, field,
+ strlen(field)) == 0
+ && line[strlen(field)] == ':') {
+ olen += snprintf(out + olen,
+ sizeof(out) - olen,
+ "%s: %s\n", field, value);
+ found = 1;
+ } else {
+ olen += snprintf(out + olen,
+ sizeof(out) - olen,
+ "%s\n", line);
+ }
+ }
+
+ free(buf);
+
+ if (!found)
+ return -1;
+
+ fp = fopen(path, "w");
+ if (!fp)
+ return -1;
+ fwrite(out, 1, olen, fp);
+ fclose(fp);
+ return 0;
+}
+
+int
+doc_sync_title(TaskDB *tdb, const char *listname,
+ int task_id)
+{
+ char path[1024], title[512];
+
+ doc_path(tdb, listname, task_id, path, sizeof(path));
+
+ if (access(path, F_OK) != 0)
+ return -1;
+
+ if (parse_title(path, title, sizeof(title)) < 0)
+ return -1;
+
+ if (title[0] == '\0')
+ return -1;
+
+ return task_update_text(tdb, task_id, title);
+}
+
+/* called from data.c when task_done/task_undo changes */
+void
+doc_sync_done(TaskDB *tdb, const char *listname,
+ int task_id, int done)
+{
+ char path[1024];
+
+ doc_path(tdb, listname, task_id, path, sizeof(path));
+
+ if (access(path, F_OK) != 0)
+ return;
+
+ update_fm_field(path, "done",
+ done ? "true" : "false");
+}
M stask.c => stask.c +60 -9
@@ 20,7 20,9 @@ usage(void)
" stask <list> add <text>\n"
" stask <list> done <id>\n"
" stask <list> undo <id>\n"
- " stask <list> del <id>\n");
+ " stask <list> del <id>\n"
+ " stask <list> edit <id>\n"
+ " stask <list> show <id>\n");
exit(1);
}
@@ 101,6 103,7 @@ cmd_new(TaskDB *tdb, const char *name)
static void
cmd_rm(TaskDB *tdb, const char *name)
{
+ doc_delete_all(tdb, name);
if (list_delete(tdb, name) < 0)
fprintf(stderr,
"stask: cannot remove '%s'\n", name);
@@ 112,6 115,8 @@ static void
cmd_add(TaskDB *tdb, const char *listname, const char *text)
{
List list;
+ Task *tasks;
+ int n, last_id;
if (list_by_name(tdb, listname, &list) < 0) {
fprintf(stderr,
@@ 119,36 124,74 @@ cmd_add(TaskDB *tdb, const char *listname, const char *text)
listname);
return;
}
- if (task_add(tdb, list.id, text) < 0)
+ if (task_add(tdb, list.id, text) < 0) {
fprintf(stderr, "stask: cannot add task\n");
- else
+ return;
+ }
+
+ /* find the id of the task we just added */
+ if (task_all(tdb, list.id, &tasks, &n) < 0 || n == 0) {
printf("Added to '%s'\n", listname);
+ return;
+ }
+ last_id = tasks[n - 1].id;
+
+ /* check all tasks — the newest has the highest id */
+ {
+ int i;
+ for (i = 0; i < n; i++) {
+ if (tasks[i].id > last_id)
+ last_id = tasks[i].id;
+ }
+ }
+
+ doc_create(tdb, listname, last_id,
+ text, tasks[n - 1].added);
+ free(tasks);
+ printf("Added to '%s' (id: %d)\n", listname, last_id);
}
static void
-cmd_done(TaskDB *tdb, int id)
+cmd_done(TaskDB *tdb, const char *listname, int id)
{
if (task_done(tdb, id) < 0)
fprintf(stderr,
"stask: cannot mark done: %d\n", id);
+ else
+ doc_sync_done(tdb, listname, id, 1);
}
static void
-cmd_undo(TaskDB *tdb, int id)
+cmd_undo(TaskDB *tdb, const char *listname, int id)
{
if (task_undo(tdb, id) < 0)
fprintf(stderr,
"stask: cannot undo: %d\n", id);
+ else
+ doc_sync_done(tdb, listname, id, 0);
}
static void
-cmd_del(TaskDB *tdb, int id)
+cmd_del(TaskDB *tdb, const char *listname, int id)
{
+ doc_delete(tdb, listname, id);
if (task_del(tdb, id) < 0)
fprintf(stderr,
"stask: cannot delete: %d\n", id);
}
+static void
+cmd_edit(TaskDB *tdb, const char *listname, int id)
+{
+ doc_edit(tdb, listname, id);
+}
+
+static void
+cmd_show_doc(TaskDB *tdb, const char *listname, int id)
+{
+ doc_show(tdb, listname, id);
+}
+
int
main(int argc, char *argv[])
{
@@ 188,15 231,23 @@ main(int argc, char *argv[])
} else if (strcmp(argv[2], "done") == 0) {
if (argc < 4) usage();
id = atoi(argv[3]);
- cmd_done(&tdb, id);
+ cmd_done(&tdb, argv[1], id);
} else if (strcmp(argv[2], "undo") == 0) {
if (argc < 4) usage();
id = atoi(argv[3]);
- cmd_undo(&tdb, id);
+ cmd_undo(&tdb, argv[1], id);
} else if (strcmp(argv[2], "del") == 0) {
if (argc < 4) usage();
id = atoi(argv[3]);
- cmd_del(&tdb, id);
+ cmd_del(&tdb, argv[1], id);
+ } else if (strcmp(argv[2], "edit") == 0) {
+ if (argc < 4) usage();
+ id = atoi(argv[3]);
+ cmd_edit(&tdb, argv[1], id);
+ } else if (strcmp(argv[2], "show") == 0) {
+ if (argc < 4) usage();
+ id = atoi(argv[3]);
+ cmd_show_doc(&tdb, argv[1], id);
} else {
usage();
}
M stask.h => stask.h +20 -0
@@ 39,6 39,7 @@ typedef struct {
typedef struct {
sqlite3 *db;
char dbpath[1024];
+ char docdir[1024];
} TaskDB;
/* data.c */
@@ 56,6 57,25 @@ int task_done(TaskDB *tdb, int task_id);
int task_undo(TaskDB *tdb, int task_id);
int task_all(TaskDB *tdb, int list_id, Task **out, int *count);
int task_count(TaskDB *tdb, int list_id, int *total, int *done);
+int task_get(TaskDB *tdb, int task_id, Task *out);
+int task_update_text(TaskDB *tdb, int task_id, const char *text);
+char *task_list_name(TaskDB *tdb, int list_id);
+
+/* doc.c */
+void doc_path(TaskDB *tdb, const char *listname, int task_id,
+ char *buf, int len);
+void doc_dir(TaskDB *tdb, const char *listname,
+ char *buf, int len);
+int doc_create(TaskDB *tdb, const char *listname,
+ int task_id, const char *title, const char *date);
+int doc_delete(TaskDB *tdb, const char *listname, int task_id);
+int doc_delete_all(TaskDB *tdb, const char *listname);
+int doc_edit(TaskDB *tdb, const char *listname, int task_id);
+int doc_show(TaskDB *tdb, const char *listname, int task_id);
+int doc_sync_title(TaskDB *tdb, const char *listname,
+ int task_id);
+void doc_sync_done(TaskDB *tdb, const char *listname,
+ int task_id, int done);
/* gui.c */
int gui_run(TaskDB *tdb, int argc, char *argv[]);