~kris/hacks

stask

6000827889e5288301e63b20e2aeff245801679c — Kris Yotam 5 months ago e95edef
v0.2: add markdown document system for tasks

each task now gets a .md file in ~/.local/share/stask/docs/<list>/<id>.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 <id>, show <id>. delete/rm cleans up doc files.
6 files changed, 484 insertions(+), 11 deletions(-)

M Makefile
M config.mk
M data.c
A doc.c
M stask.c
M stask.h
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[]);