task

creates a plain-text task file in the current directory
git clone git://brookjeynes.dev/bjeynes/task.git
Log | Files | Refs | README | LICENSE

main.c (12216B)


      1 #define _DEFAULT_SOURCE
      2 #include <dirent.h>
      3 #include <errno.h>
      4 #include <stdint.h>
      5 #include <stdio.h>
      6 #include <stdlib.h>
      7 #include <string.h>
      8 #include <sys/random.h>
      9 #include <unistd.h>
     10 
     11 #define TASK_ID_BYTES 8
     12 #define TASK_ID_CHARS (TASK_ID_BYTES * 2)
     13 #define TASKS_DIR "."
     14 #define DONE_TASKS_DIR "done"
     15 
     16 static const char reverse_hex[] = "klmnopqrstuvwxyz";
     17 
     18 struct options {
     19   char *title;
     20   char priority;
     21 };
     22 
     23 static void print_help(const char *program) {
     24   printf("usage: %s [--priority A|-p A] <title> [properties...]\n", program);
     25   printf("\n");
     26   printf("create a new task.\n");
     27   printf("\n");
     28   printf("options:\n");
     29   printf("  -h, --help           show this help\n");
     30   printf("  -p, --priority A     set priority A-Z\n");
     31   printf("  --context            list all known contexts\n");
     32   printf("  --projects           list all known projects\n");
     33 }
     34 
     35 int has_txt_extension(const char *name) {
     36   size_t len = strlen(name);
     37   return len >= 4 && strcmp(name + len - 4, ".txt") == 0;
     38 }
     39 
     40 int generate_task_id(char out[TASK_ID_CHARS + 1]) {
     41   uint8_t bytes[TASK_ID_BYTES];
     42 
     43   size_t got = 0;
     44   while (got < sizeof(bytes)) {
     45     ssize_t n = getrandom(bytes + got, sizeof(bytes) - got, 0);
     46     if (n == -1) {
     47       return -1;
     48     }
     49     got += n;
     50   }
     51 
     52   for (size_t i = 0; i < TASK_ID_BYTES; i++) {
     53     out[i * 2] = reverse_hex[bytes[i] >> 4];
     54     out[i * 2 + 1] = reverse_hex[bytes[i] & 0x0f];
     55   }
     56 
     57   out[TASK_ID_CHARS] = '\0';
     58   return 0;
     59 }
     60 
     61 int task_id_exists_in_dir(const char *dir, const char *id, int allow_missing) {
     62   struct dirent *dp;
     63   FILE *fd = NULL;
     64   int rc = -1;
     65 
     66   DIR *dirp = opendir(dir);
     67   if (dirp == NULL) {
     68     if (allow_missing && errno == ENOENT) {
     69       return 0;
     70     }
     71 
     72     fprintf(stderr, "err: failed to open directory: %s\n", strerror(errno));
     73     goto cleanup;
     74   }
     75 
     76   for (;;) {
     77     errno = 0;
     78     dp = readdir(dirp);
     79     if (dp == NULL) {
     80       if (errno == 0) {
     81         break;
     82       }
     83       fprintf(stderr, "err: failed to iterate directory: %s\n",
     84               strerror(errno));
     85       goto cleanup;
     86     }
     87 
     88     if (dp->d_type != DT_REG) {
     89       continue;
     90     }
     91 
     92     if (has_txt_extension(dp->d_name) == 0) {
     93       continue;
     94     }
     95 
     96     char path[1024];
     97     int path_len;
     98     if (strcmp(dir, ".") == 0) {
     99       path_len = snprintf(path, sizeof path, "%s", dp->d_name);
    100     } else {
    101       path_len = snprintf(path, sizeof path, "%s/%s", dir, dp->d_name);
    102     }
    103 
    104     if (path_len < 0 || (size_t)path_len >= sizeof path) {
    105       fprintf(stderr, "err: task file path too long\n");
    106       goto cleanup;
    107     }
    108 
    109     fd = fopen(path, "r");
    110     if (fd == NULL) {
    111       fprintf(stderr, "err: failed to open file: %s\n", strerror(errno));
    112       goto cleanup;
    113     }
    114 
    115     char line[1024];
    116     if (fgets(line, sizeof line, fd) == NULL) {
    117       if (ferror(fd)) {
    118         fprintf(stderr, "err: failed to read file: %s\n", strerror(errno));
    119         goto cleanup;
    120       }
    121 
    122       // empty file
    123       if (fclose(fd) != 0) {
    124         fprintf(stderr, "err: failed to close file: %s\n", strerror(errno));
    125         goto cleanup;
    126       }
    127 
    128       fd = NULL;
    129       continue;
    130     }
    131 
    132     if (fgets(line, sizeof line, fd) == NULL) {
    133       if (ferror(fd)) {
    134         fprintf(stderr, "err: failed to read file: %s\n", strerror(errno));
    135         goto cleanup;
    136       }
    137 
    138       if (fclose(fd) != 0) {
    139         fprintf(stderr, "err: failed to close file: %s\n", strerror(errno));
    140         goto cleanup;
    141       }
    142 
    143       fd = NULL;
    144       continue;
    145     }
    146 
    147     char expected[TASK_ID_CHARS + 6]; // "id: " + 16 + "\n" + "\0"
    148     snprintf(expected, sizeof expected, "id: %s\n", id);
    149     if (strcmp(line, expected) == 0) {
    150       rc = 1;
    151       goto cleanup;
    152     }
    153 
    154     if (fclose(fd) != 0) {
    155       fd = NULL;
    156       fprintf(stderr, "err: failed to close file: %s\n", strerror(errno));
    157       goto cleanup;
    158     }
    159     fd = NULL;
    160   }
    161 
    162   rc = 0;
    163 
    164 cleanup:
    165   if (fd != NULL) {
    166     if (fclose(fd) != 0) {
    167       fprintf(stderr, "err: failed to close file: %s\n", strerror(errno));
    168       rc = -1;
    169     }
    170   }
    171 
    172   if (dirp != NULL && closedir(dirp) != 0) {
    173     fprintf(stderr, "err: failed to close directory: %s\n", strerror(errno));
    174     rc = -1;
    175   }
    176 
    177   return rc;
    178 }
    179 
    180 int task_id_exists(const char *id) {
    181   int exists = task_id_exists_in_dir(TASKS_DIR, id, 0);
    182   if (exists != 0) {
    183     return exists;
    184   }
    185 
    186   return task_id_exists_in_dir(DONE_TASKS_DIR, id, 1);
    187 }
    188 
    189 int generate_unique_task_id(char id[TASK_ID_CHARS + 1]) {
    190   for (;;) {
    191     if (generate_task_id(id) != 0) {
    192       return -1;
    193     }
    194 
    195     int exists = task_id_exists(id);
    196     if (exists == -1) {
    197       return -1;
    198     }
    199 
    200     if (!exists) {
    201       return 0;
    202     }
    203   }
    204 }
    205 
    206 static void print_properties(char identifier) {
    207   struct dirent *dp;
    208   FILE *fd = NULL;
    209   int rc = 1;
    210 
    211   char *properties[1024];
    212   size_t properties_len = 0;
    213 
    214   DIR *dirp = opendir(TASKS_DIR);
    215   if (dirp == NULL) {
    216     fprintf(stderr, "err: failed to open directory: %s\n", strerror(errno));
    217     goto cleanup;
    218   }
    219 
    220   for (;;) {
    221     errno = 0;
    222     dp = readdir(dirp);
    223     if (dp == NULL) {
    224       if (errno == 0) {
    225         break;
    226       }
    227       fprintf(stderr, "err: failed to iterate directory: %s\n",
    228               strerror(errno));
    229       goto cleanup;
    230     }
    231 
    232     if (dp->d_type != DT_REG) {
    233       continue;
    234     }
    235 
    236     if (has_txt_extension(dp->d_name) == 0) {
    237       continue;
    238     }
    239 
    240     fd = fopen(dp->d_name, "r");
    241     if (fd == NULL) {
    242       fprintf(stderr, "err: failed to open file: %s\n", strerror(errno));
    243       goto cleanup;
    244     }
    245 
    246     char line[1024];
    247     if (fgets(line, sizeof line, fd) == NULL) {
    248       if (ferror(fd)) {
    249         fprintf(stderr, "err: failed to read file: %s\n", strerror(errno));
    250         goto cleanup;
    251       }
    252 
    253       // empty file
    254       if (fclose(fd) != 0) {
    255         fprintf(stderr, "err: failed to close file: %s\n", strerror(errno));
    256         goto cleanup;
    257       }
    258 
    259       fd = NULL;
    260       continue;
    261     }
    262 
    263     size_t byte = 0;
    264     while (line[byte] != '\0') {
    265       char c = line[byte];
    266 
    267       if (c == identifier) {
    268         char property[1024];
    269         size_t property_len = 0;
    270 
    271         while (line[byte] != ' ' && line[byte] != '\n' && line[byte] != '\0' &&
    272                property_len < sizeof(property) - 1) {
    273           property[property_len] = line[byte];
    274           property_len++;
    275           byte++;
    276         }
    277         property[property_len] = '\0';
    278         property_len++;
    279 
    280         char *property_ptr = malloc(sizeof(char) * property_len);
    281         if (property_ptr == NULL) {
    282           fprintf(stderr, "err: failed to alloc space for read property: %s\n",
    283                   strerror(errno));
    284           goto cleanup;
    285         }
    286 
    287         if (property_len < sizeof(property)) {
    288           memcpy(property_ptr, property, property_len);
    289         }
    290 
    291         if (properties_len == sizeof(properties) / sizeof(properties[0])) {
    292           fprintf(stderr, "err: too many properties\n");
    293           free(property_ptr);
    294           goto cleanup;
    295         }
    296 
    297         properties[properties_len] = property_ptr;
    298         properties_len++;
    299       } else {
    300         byte++;
    301       }
    302     }
    303 
    304     if (fclose(fd) != 0) {
    305       fd = NULL;
    306       fprintf(stderr, "err: failed to close file: %s\n", strerror(errno));
    307       goto cleanup;
    308     }
    309     fd = NULL;
    310   }
    311 
    312   rc = 0;
    313 
    314   for (size_t i = 0; i < properties_len; i++) {
    315     int is_duplicate = 0;
    316     for (size_t j = i; j > 0; j--) {
    317       if (strcmp(properties[i], properties[j - 1]) == 0) {
    318         is_duplicate = 1;
    319         break;
    320       }
    321     }
    322 
    323     if (is_duplicate == 1) {
    324       continue;
    325     }
    326 
    327     printf("%s\n", properties[i]);
    328   }
    329 
    330 cleanup:
    331   if (fd != NULL) {
    332     fclose(fd);
    333   }
    334 
    335   for (size_t i = 0; i < properties_len; i++) {
    336     free(properties[i]);
    337   }
    338 
    339   if (dirp != NULL && closedir(dirp) != 0 && rc == 0) {
    340     fprintf(stderr, "err: failed to close directory: %s\n", strerror(errno));
    341     rc = 1;
    342   }
    343 
    344   if (rc != 0) {
    345     exit(1);
    346   }
    347 }
    348 
    349 int main(int argc, char *argv[]) {
    350   struct options cli_args = {0};
    351   cli_args.priority = '\0';
    352 
    353   if (argc > 1 &&
    354       (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-h") == 0)) {
    355     print_help(argv[0]);
    356     return 0;
    357   }
    358 
    359   if (argc > 1 && (strcmp(argv[1], "--contexts") == 0)) {
    360     print_properties('@');
    361     return 0;
    362   }
    363 
    364   if (argc > 1 && (strcmp(argv[1], "--projects") == 0)) {
    365     print_properties('+');
    366     return 0;
    367   }
    368 
    369   if (argc == 1) {
    370     fprintf(stderr, "err: title required\n");
    371     exit(1);
    372   }
    373 
    374   int positionals = 0;
    375   for (int idx = 1; idx < argc; idx++) {
    376     if (strcmp(argv[idx], "--priority") == 0 || strcmp(argv[idx], "-p") == 0) {
    377       if (cli_args.priority != '\0') {
    378         fprintf(stderr, "err: priority specified multiple times\n");
    379         exit(1);
    380       }
    381 
    382       if (idx + 1 == argc) {
    383         fprintf(stderr, "err: priority required\n");
    384         exit(1);
    385       }
    386 
    387       if (strcmp(argv[idx + 1], "") == 0) {
    388         fprintf(stderr, "err: priority can not be empty\n");
    389         exit(1);
    390       }
    391 
    392       if (strlen(argv[idx + 1]) != 1) {
    393         fprintf(stderr,
    394                 "err: priority must be a single letter between [A-Z]\n");
    395         exit(1);
    396       }
    397 
    398       cli_args.priority = argv[idx + 1][0];
    399       idx++;
    400       continue;
    401     }
    402 
    403     if (strcmp(argv[idx], "") == 0) {
    404       fprintf(stderr, "err: argument can not be empty\n");
    405       exit(1);
    406     }
    407 
    408     if (positionals == 0) {
    409       cli_args.title = argv[idx];
    410     }
    411 
    412     positionals++;
    413   }
    414 
    415   if (cli_args.title == NULL) {
    416     fprintf(stderr, "err: title required\n");
    417     exit(1);
    418   }
    419 
    420   if (cli_args.priority != '\0') {
    421     if (cli_args.priority < 'A' || cli_args.priority > 'Z') {
    422       fprintf(stderr, "err: priority must be between [A-Z]\n");
    423       exit(1);
    424     }
    425   }
    426 
    427   size_t title_len = strlen(cli_args.title);
    428   if (title_len > 251) {
    429     fprintf(stderr, "err: title too long for file name\n");
    430     exit(1);
    431   }
    432 
    433   char title_to_file[256];
    434   int in_leading_dots = 1;
    435   for (size_t idx = 0; idx < title_len; idx++) {
    436     if ((in_leading_dots && cli_args.title[idx] == '.') ||
    437         cli_args.title[idx] == ' ' || cli_args.title[idx] == '/' ||
    438         cli_args.title[idx] == '\\') {
    439       title_to_file[idx] = '_';
    440     } else {
    441       in_leading_dots = 0;
    442       title_to_file[idx] = cli_args.title[idx];
    443     }
    444   }
    445   strcpy(title_to_file + title_len, ".txt");
    446 
    447   char id[TASK_ID_CHARS + 1];
    448   if (generate_unique_task_id(id) != 0) {
    449     perror("generate_unique_task_id");
    450     return 1;
    451   }
    452 
    453   FILE *f = fopen(title_to_file, "wx");
    454   if (f == NULL) {
    455     fprintf(stderr, "err: failed to open file: %s\n", strerror(errno));
    456     exit(1);
    457   }
    458 
    459   int write_failed = 0;
    460   int write_errno = 0;
    461   if (cli_args.priority != '\0') {
    462     if (fprintf(f, "(%c) ", cli_args.priority) < 0) {
    463       write_failed = 1;
    464       write_errno = errno;
    465     }
    466   }
    467 
    468   if (fprintf(f, "%s", cli_args.title) < 0) {
    469     if (!write_failed) {
    470       write_failed = 1;
    471       write_errno = errno;
    472     }
    473   }
    474 
    475   int seen_title = 0;
    476   for (int idx = 1; idx < argc; idx++) {
    477     if (strcmp(argv[idx], "--priority") == 0 || strcmp(argv[idx], "-p") == 0) {
    478       idx++;
    479       continue;
    480     }
    481 
    482     seen_title++;
    483     if (seen_title == 1) {
    484       continue;
    485     }
    486 
    487     if (fprintf(f, " %s", argv[idx]) < 0) {
    488       if (!write_failed) {
    489         write_failed = 1;
    490         write_errno = errno;
    491       }
    492     }
    493   }
    494 
    495   if (fprintf(f, "\n") < 0) {
    496     if (!write_failed) {
    497       write_failed = 1;
    498       write_errno = errno;
    499     }
    500   }
    501 
    502   if (fprintf(f, "id: %s\n", id) < 0) {
    503     if (!write_failed) {
    504       write_failed = 1;
    505       write_errno = errno;
    506     }
    507   }
    508 
    509   int close_failed = 0;
    510   int close_errno = 0;
    511   if (fclose(f) != 0) {
    512     close_failed = 1;
    513     close_errno = errno;
    514   }
    515 
    516   if (write_failed) {
    517     fprintf(stderr, "err: failed to write file: %s\n", strerror(write_errno));
    518     exit(1);
    519   }
    520 
    521   if (close_failed) {
    522     fprintf(stderr, "err: failed to close file: %s\n", strerror(close_errno));
    523     exit(1);
    524   }
    525 
    526   char *editor = getenv("VISUAL");
    527   if (editor != NULL && strcmp(editor, "") == 0) {
    528     editor = NULL;
    529   }
    530 
    531   if (editor == NULL) {
    532     editor = getenv("EDITOR");
    533   }
    534 
    535   if (editor != NULL && strcmp(editor, "") == 0) {
    536     editor = NULL;
    537   }
    538 
    539   if (editor == NULL) {
    540     return 0;
    541   }
    542 
    543   execlp("sh", "sh", "-c", "exec $1 \"$2\"", "task", editor, title_to_file,
    544          NULL);
    545   perror("err: failed to open editor");
    546   return 1;
    547 }