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 }