From 6000827889e5288301e63b20e2aeff245801679c Mon Sep 17 00:00:00 2001 From: Kris Yotam Date: Sat, 21 Mar 2026 00:19:54 -0500 Subject: [PATCH] v0.2: add markdown document system for tasks each task now gets a .md file in ~/.local/share/stask/docs//.md with yaml frontmatter (title, date, done). done/undo syncs to frontmatter. edit opens $EDITOR, show prints doc to stdout. title syncs back to db on edit. new commands: edit , show . delete/rm cleans up doc files. --- Makefile | 2 +- config.mk | 2 +- data.c | 78 +++++++++++++ doc.c | 324 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ stask.c | 69 ++++++++++-- stask.h | 20 ++++ 6 files changed, 484 insertions(+), 11 deletions(-) create mode 100644 doc.c diff --git a/Makefile b/Makefile index acd51c4b5fe6312d1192a1cdf37e89f902067227..f6a4c06d2b4d9ce3b74c852c09e5c0563ac98da1 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/config.mk b/config.mk index 89195aeb96fc8bb7911a3b530be659a751559eba..2d032804ebd18e9a5bd159984fb18a766cde9466 100644 --- a/config.mk +++ b/config.mk @@ -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 diff --git a/data.c b/data.c index b4bb65fed2cd8fc0ca1c86e0568db28528f05ea6..32635467d0bc26f4a123e1e6fea130e98c0a905f 100644 --- a/data.c +++ b/data.c @@ -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; +} diff --git a/doc.c b/doc.c new file mode 100644 index 0000000000000000000000000000000000000000..caf004648f21db5ecbdc884e63cf272e11ab5637 --- /dev/null +++ b/doc.c @@ -0,0 +1,324 @@ +/* stask - simple task manager + * See LICENSE file for copyright and license details. */ + +#include +#include +#include +#include +#include + +#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"); +} diff --git a/stask.c b/stask.c index ad47a30328822e1510ad62fa1b14a22ea747afb2..010f6b7f9bd6ee76d040bdc63bb8fb68b27c64db 100644 --- a/stask.c +++ b/stask.c @@ -20,7 +20,9 @@ usage(void) " stask add \n" " stask done \n" " stask undo \n" - " stask del \n"); + " stask del \n" + " stask edit \n" + " stask show \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(); } diff --git a/stask.h b/stask.h index 9995fcb54504abb9ab77decc008b22c57bb3ab0d..8791736cc155a1639c367c860f419e6e562f433b 100644 --- a/stask.h +++ b/stask.h @@ -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[]);