commit 9fd901758d3877caa5fca48f9e446a13b7630193
parent 54ec2336968986b96ad46ff3adc73d9677bc25c0
Author: brookjeynes <me@brookjeynes.dev>
Date: Thu, 25 Jun 2026 07:57:20 +1000
refactor: consolidate contexts / projects into single sub-command
Task-id: nrqnlxvtksoyytvz
Task-id: xwqnwoxwwtywnpry
Signed-off-by: brookjeynes <me@brookjeynes.dev>
Diffstat:
5 files changed, 149 insertions(+), 92 deletions(-)
diff --git a/README b/README
@@ -1,14 +1,14 @@
task
====
-Create a plain-text task file in the current directory.
+Create a plain-text task file in tasks/
Usage
-----
- $ task "Call Mom"
- $ task -p A "Call Mom" @phone +family
+ $ task new "Call Mom"
+ $ task new -p A "Call Mom" @phone +family
The task title becomes the filename. Spaces, slashes, backslashes, and leading
dots are replaced with underscores.
diff --git a/task.c b/task.c
@@ -8,12 +8,17 @@
#include <stdlib.h>
#include <string.h>
#include <sys/random.h>
+#include <sys/stat.h>
#include <unistd.h>
#define TASK_ID_BYTES 8
#define TASK_ID_CHARS (TASK_ID_BYTES * 2)
-#define TASKS_DIR "."
+#define TASKS_DIR "tasks"
+#define TASKS_PATH \
+ "." \
+ "/" TASKS_DIR
#define DONE_TASKS_DIR "done"
+#define DONE_TASKS_PATH TASKS_PATH "/" DONE_TASKS_DIR
static const char reverse_hex[] = "klmnopqrstuvwxyz";
@@ -22,21 +27,43 @@ struct options {
char priority;
};
+static int filter_entries(const struct dirent *dp) {
+ if (strcmp(dp->d_name, ".") == 0 || strcmp(dp->d_name, "..") == 0) {
+ return 0;
+ }
+ if (strcmp(dp->d_name, DONE_TASKS_DIR) == 0) {
+ return 0;
+ }
+ return 1;
+}
+
+static int count_files_scandir(const char *dir_path) {
+ struct dirent **namelist;
+ int count;
+
+ count = scandir(dir_path, &namelist, filter_entries, alphasort);
+ if (count == -1) {
+ return -1;
+ }
+
+ for (int i = 0; i < count; i++) {
+ free(namelist[i]);
+ }
+ free(namelist);
+
+ return count;
+}
+
static void print_help(const char *program) {
- printf("usage: %s <command> [options]\n",
+ printf("usage: %s <command> [options]\n\n"
+ "commands:\n"
+ " commits <task-id> list all commits tied <task-id>.\n"
+ " new <title> [properties...] create a new task.\n"
+ " summary list contexts, projects, etc.\n"
+ "\noptions:\n"
+ " -h, --help show this help\n"
+ " -p, --priority A set priority A-Z\n",
program);
- printf("\n");
- printf("create a new task.\n");
- printf("\n");
- printf("commands:\n");
- printf(" commits <task-id> list all commits tied <task-id>.\n");
- printf(" new [--priority A|-p A] <title> [properties...] create a new task.\n");
- printf("\n");
- printf("options:\n");
- printf(" -h, --help show this help\n");
- printf(" -p, --priority A set priority A-Z\n");
- printf(" --context list all known contexts\n");
- printf(" --projects list all known projects\n");
}
static int has_txt_extension(const char *name) {
@@ -77,7 +104,7 @@ static int task_id_exists_in_dir(const char *dir, const char *id,
return 0;
}
- fprintf(stderr, "err: failed to open directory: %s\n", strerror(errno));
+ fprintf(stderr, "err: failed to open %s: %s\n", dir, strerror(errno));
goto cleanup;
}
@@ -88,8 +115,7 @@ static int task_id_exists_in_dir(const char *dir, const char *id,
if (errno == 0) {
break;
}
- fprintf(stderr, "err: failed to iterate directory: %s\n",
- strerror(errno));
+ perror("err: failed to iterate directory");
goto cleanup;
}
@@ -101,7 +127,7 @@ static int task_id_exists_in_dir(const char *dir, const char *id,
continue;
}
- char path[1024];
+ char path[4096];
int path_len;
if (strcmp(dir, ".") == 0) {
path_len = snprintf(path, sizeof path, "%s", dp->d_name);
@@ -116,20 +142,20 @@ static int task_id_exists_in_dir(const char *dir, const char *id,
fd = fopen(path, "r");
if (fd == NULL) {
- fprintf(stderr, "err: failed to open file: %s\n", strerror(errno));
+ perror("err: failed to open file");
goto cleanup;
}
char line[1024];
if (fgets(line, sizeof line, fd) == NULL) {
if (ferror(fd)) {
- fprintf(stderr, "err: failed to read file: %s\n", strerror(errno));
+ perror("err: failed to read file");
goto cleanup;
}
// empty file
if (fclose(fd) != 0) {
- fprintf(stderr, "err: failed to close file: %s\n", strerror(errno));
+ perror("err: failed to close file");
goto cleanup;
}
@@ -139,12 +165,12 @@ static int task_id_exists_in_dir(const char *dir, const char *id,
if (fgets(line, sizeof line, fd) == NULL) {
if (ferror(fd)) {
- fprintf(stderr, "err: failed to read file: %s\n", strerror(errno));
+ perror("err: failed to read file");
goto cleanup;
}
if (fclose(fd) != 0) {
- fprintf(stderr, "err: failed to close file: %s\n", strerror(errno));
+ perror("err: failed to close file");
goto cleanup;
}
@@ -161,7 +187,7 @@ static int task_id_exists_in_dir(const char *dir, const char *id,
if (fclose(fd) != 0) {
fd = NULL;
- fprintf(stderr, "err: failed to close file: %s\n", strerror(errno));
+ perror("err: failed to close file");
goto cleanup;
}
fd = NULL;
@@ -172,13 +198,13 @@ static int task_id_exists_in_dir(const char *dir, const char *id,
cleanup:
if (fd != NULL) {
if (fclose(fd) != 0) {
- fprintf(stderr, "err: failed to close file: %s\n", strerror(errno));
+ perror("err: failed to close file");
rc = -1;
}
}
if (dirp != NULL && closedir(dirp) != 0) {
- fprintf(stderr, "err: failed to close directory: %s\n", strerror(errno));
+ perror("err: failed to close directory");
rc = -1;
}
@@ -186,12 +212,12 @@ cleanup:
}
static int task_id_exists(const char *id) {
- int exists = task_id_exists_in_dir(TASKS_DIR, id, 0);
+ int exists = task_id_exists_in_dir(TASKS_PATH, id, 0);
if (exists != 0) {
return exists;
}
- return task_id_exists_in_dir(DONE_TASKS_DIR, id, 1);
+ return task_id_exists_in_dir(DONE_TASKS_PATH, id, 1);
}
static int generate_unique_task_id(char id[TASK_ID_CHARS + 1]) {
@@ -219,9 +245,9 @@ static void print_properties(char identifier) {
char *properties[1024];
size_t properties_len = 0;
- DIR *dirp = opendir(TASKS_DIR);
+ DIR *dirp = opendir(TASKS_PATH);
if (dirp == NULL) {
- fprintf(stderr, "err: failed to open directory: %s\n", strerror(errno));
+ perror("err: failed to open tasks directory");
goto cleanup;
}
@@ -232,8 +258,7 @@ static void print_properties(char identifier) {
if (errno == 0) {
break;
}
- fprintf(stderr, "err: failed to iterate directory: %s\n",
- strerror(errno));
+ perror("err: failed to iterate directory");
goto cleanup;
}
@@ -245,22 +270,25 @@ static void print_properties(char identifier) {
continue;
}
- fd = fopen(dp->d_name, "r");
+ char path[4096];
+ snprintf(path, sizeof(path), "%s/%s", TASKS_PATH, dp->d_name);
+
+ fd = fopen(path, "r");
if (fd == NULL) {
- fprintf(stderr, "err: failed to open file: %s\n", strerror(errno));
+ perror("err: failed to open file");
goto cleanup;
}
char line[1024];
if (fgets(line, sizeof line, fd) == NULL) {
if (ferror(fd)) {
- fprintf(stderr, "err: failed to read file: %s\n", strerror(errno));
+ perror("err: failed to read file");
goto cleanup;
}
// empty file
if (fclose(fd) != 0) {
- fprintf(stderr, "err: failed to close file: %s\n", strerror(errno));
+ perror("err: failed to close file");
goto cleanup;
}
@@ -287,8 +315,7 @@ static void print_properties(char identifier) {
char *property_ptr = malloc(sizeof(char) * property_len);
if (property_ptr == NULL) {
- fprintf(stderr, "err: failed to alloc space for read property: %s\n",
- strerror(errno));
+ perror("err: failed to alloc space for read property");
goto cleanup;
}
@@ -311,7 +338,7 @@ static void print_properties(char identifier) {
if (fclose(fd) != 0) {
fd = NULL;
- fprintf(stderr, "err: failed to close file: %s\n", strerror(errno));
+ perror("err: failed to close file");
goto cleanup;
}
fd = NULL;
@@ -345,7 +372,7 @@ cleanup:
}
if (dirp != NULL && closedir(dirp) != 0 && rc == 0) {
- fprintf(stderr, "err: failed to close directory: %s\n", strerror(errno));
+ perror("err: failed to close directory");
rc = 1;
}
@@ -354,7 +381,7 @@ cleanup:
}
}
-int new_command(int argc, char *argv[], struct options *cli_args) {
+static int new_command(int argc, char *argv[], struct options *cli_args) {
if (argc == 1) {
fprintf(stderr, "err: title required\n");
return 1;
@@ -433,32 +460,42 @@ int new_command(int argc, char *argv[], struct options *cli_args) {
}
strcpy(title_to_file + title_len, ".txt");
- char id[TASK_ID_CHARS + 1];
- if (generate_unique_task_id(id) != 0) {
- perror("generate_unique_task_id");
+ char path[4096];
+ snprintf(path, sizeof(path), "%s/%s", TASKS_PATH, title_to_file);
+
+ struct stat st = {0};
+ if (stat(TASKS_PATH, &st) == -1) {
+ mkdir(TASKS_PATH, 0700);
+ }
+
+ DIR *tasks_dir = opendir(TASKS_PATH);
+ if (tasks_dir == NULL) {
+ if (errno == ENOENT) {
+ }
+
+ fprintf(stderr, "err: failed to check for %s: %s\n", TASKS_PATH,
+ strerror(errno));
return 1;
}
+ closedir(tasks_dir);
- FILE *f = fopen(title_to_file, "wx");
+ FILE *f = fopen(path, "wx");
if (f == NULL) {
- fprintf(stderr, "err: failed to open file: %s\n", strerror(errno));
+ perror("err: failed to open files");
return 1;
}
- int write_failed = 0;
- int write_errno = 0;
-
if (cli_args->priority != '\0') {
if (fprintf(f, "(%c) ", cli_args->priority) < 0) {
fclose(f);
- fprintf(stderr, "err: failed to write file: %s\n", strerror(errno));
+ perror("err: failed to write file");
return 1;
}
}
if (fprintf(f, "%s", cli_args->title) < 0) {
fclose(f);
- fprintf(stderr, "err: failed to write file: %s\n", strerror(errno));
+ perror("err: failed to write file");
return 1;
}
@@ -476,43 +513,49 @@ int new_command(int argc, char *argv[], struct options *cli_args) {
if (fprintf(f, " %s", argv[idx]) < 0) {
fclose(f);
- fprintf(stderr, "err: failed to write file: %s\n", strerror(errno));
+ perror("err: failed to write file");
return 1;
}
}
if (fprintf(f, "\n") < 0) {
fclose(f);
- fprintf(stderr, "err: failed to write file: %s\n", strerror(errno));
+ perror("err: failed to write file");
return 1;
}
- if (fprintf(f, "id: %s\n", id) < 0) {
- fclose(f);
- fprintf(stderr, "err: failed to write file: %s\n", strerror(errno));
+ char id[TASK_ID_CHARS + 1];
+ if (generate_unique_task_id(id) != 0) {
+ perror("err: failed to generate unique id");
return 1;
}
- char *editor = getenv("VISUAL");
- if (editor != NULL && strcmp(editor, "") == 0) {
- editor = NULL;
- }
- if (editor == NULL) {
- editor = getenv("EDITOR");
+ if (fprintf(f, "id: %s\n", id) < 0) {
+ fclose(f);
+ perror("err: failed to write file");
+ return 1;
}
- if (editor == NULL || strcmp(editor, "") == 0) {
- return 0;
- }
+ // char *editor = getenv("VISUAL");
+ // if (editor != NULL && strcmp(editor, "") == 0) {
+ // editor = NULL;
+ // }
+ // if (editor == NULL) {
+ // editor = getenv("EDITOR");
+ // }
+ //
+ // if (editor == NULL || strcmp(editor, "") == 0) {
+ // return 0;
+ // }
- execlp("sh", "sh", "-c", "exec $1 \"$2\"", "task", editor, title_to_file,
- NULL);
- fprintf(stderr, "err: failed to open editor: %s", strerror(errno));
+ // execlp("sh", "sh", "-c", "exec $1 \"$2\"", "task", editor, path, NULL);
+ // perror("err: failed to open editor");
return 1;
}
-int related_commits_command(int argc, char *argv[], struct options *cli_args) {
+static int related_commits_command(int argc, char *argv[],
+ struct options *cli_args) {
if (argc != 2) {
fprintf(stderr, "err: task id required\n");
return 1;
@@ -605,16 +648,6 @@ int main(int argc, char *argv[]) {
return 0;
}
- if (argc > 1 && (strcmp(argv[1], "--contexts") == 0)) {
- print_properties('@');
- return 0;
- }
-
- if (argc > 1 && (strcmp(argv[1], "--projects") == 0)) {
- print_properties('+');
- return 0;
- }
-
if (argc > 1 && strcmp(argv[1], "new") == 0) {
if (new_command(argc - 1, argv + 1, &cli_args) != 0) {
return 1;
@@ -627,5 +660,29 @@ int main(int argc, char *argv[]) {
}
}
- return 1;
+ if (argc > 1 && strcmp(argv[1], "summary") == 0) {
+ int open_tasks;
+ if ((open_tasks = count_files_scandir(TASKS_PATH)) < 0) {
+ perror("err: failed to read tasks/");
+ return 1;
+ }
+
+ int closed_tasks;
+ if ((closed_tasks = count_files_scandir(DONE_TASKS_PATH)) < 0) {
+ if (errno != ENOENT) {
+ perror("err: failed to read tasks/done/");
+ return 1;
+ }
+ closed_tasks = 0;
+ }
+
+ printf("open tasks: %d\n", open_tasks);
+ printf("closed tasks: %d\n", closed_tasks);
+ printf("\ncontexts:\n");
+ print_properties('@');
+ printf("\nprojects:\n");
+ print_properties('+');
+ }
+
+ return 0;
}
diff --git a/tasks/add_subcommands.txt b/tasks/add_subcommands.txt
@@ -1,9 +0,0 @@
-add subcommands @feat
-id: nrqnlxvtksoyytvz
-
-task doesnt have subcommands. this is a little painful when adding new features
-as they are forced behind flags.
-
-[x] new
-[x] related-commits
-[ ] contexts / projects
diff --git a/tasks/done/add_subcommands.txt b/tasks/done/add_subcommands.txt
@@ -0,0 +1,9 @@
+add subcommands @feat
+id: nrqnlxvtksoyytvz
+
+task doesnt have subcommands. this is a little painful when adding new features
+as they are forced behind flags.
+
+[x] new
+[x] related-commits
+[x] contexts / projects
diff --git a/tasks/consolidate_contexts_and_projects_into_properties_command.txt b/tasks/done/consolidate_contexts_and_projects_into_properties_command.txt