diary.c (37009B)
1 #include "diary.h" 2 3 int cy, cx; 4 time_t raw_time; 5 struct tm today; 6 struct tm curs_date; 7 struct tm cal_start; 8 struct tm cal_end; 9 10 // normally leap is every 4 years, 11 // but is skipped every 100 years, 12 // unless it is divisible by 400 13 #define is_leap(yr) ((yr % 400 == 0) || (yr % 4 == 0 && yr % 100 != 0)) 14 15 void setup_cal_timeframe() { 16 raw_time = time(NULL); 17 localtime_r(&raw_time, &today); 18 today.tm_sec = 0; 19 today.tm_min = 0; 20 today.tm_hour = 0; 21 // ignore dst, irrelevant for the daily schedule 22 // and only overthrows calendar drawing on change days 23 today.tm_isdst = 0; 24 mktime(&today); 25 26 curs_date = today; 27 cal_start = today; 28 cal_start.tm_year -= CONFIG.range; 29 cal_start.tm_mon = 0; 30 cal_start.tm_mday = 1; 31 mktime(&cal_start); 32 33 if (cal_start.tm_wday != CONFIG.weekday) { 34 // adjust start date to weekday before 01.01 35 cal_start.tm_year--; 36 cal_start.tm_mon = 11; 37 cal_start.tm_mday = 31 - (cal_start.tm_wday - CONFIG.weekday) + 1; 38 mktime(&cal_start); 39 } 40 41 cal_end = today; 42 cal_end.tm_year += CONFIG.range; 43 cal_end.tm_mon = 11; 44 cal_end.tm_mday = 31; 45 cal_end.tm_sec = 59; 46 cal_end.tm_min = 59; 47 cal_end.tm_hour = 23; 48 cal_end.tm_isdst = 0; 49 mktime(&cal_end); 50 } 51 52 void draw_wdays(WINDOW* head) { 53 int i; 54 for (i = CONFIG.weekday; i < CONFIG.weekday + 7; i++) { 55 waddstr(head, WEEKDAYS[i % 7]); 56 waddch(head, ' '); 57 } 58 wrefresh(head); 59 } 60 61 void draw_calendar(WINDOW* number_pad, WINDOW* month_pad, const char* diary_dir, size_t diary_dir_size) { 62 struct tm i = cal_start; 63 char month[10]; 64 bool has_entry; 65 66 while (mktime(&i) <= mktime(&cal_end)) { 67 has_entry = date_has_entry(diary_dir, diary_dir_size, &i); 68 69 if (has_entry) 70 wattron(number_pad, A_BOLD); 71 72 wprintw(number_pad, "%2i", i.tm_mday); 73 74 if (has_entry) 75 wattroff(number_pad, A_BOLD); 76 77 waddch(number_pad, ' '); 78 79 // print month in sidebar 80 if (i.tm_mday == 1) { 81 strftime(month, sizeof month, "%b", &i); 82 getyx(number_pad, cy, cx); 83 mvwprintw(month_pad, cy, 0, "%s ", month); 84 } 85 86 i.tm_mday++; 87 } 88 } 89 90 /* Update window 'win' with diary entry from date 'date' */ 91 void display_entry(const char* dir, size_t dir_size, const struct tm* date, WINDOW* win, int width) { 92 char path[100]; 93 char* ppath = path; 94 FILE* fp; 95 int c; 96 97 // pty file descriptors 98 int fdm, fds; 99 char* pts = ""; 100 101 size_t entry_size, indent_size; 102 103 // formatted entry 104 char* entry = calloc(1, sizeof(char)); 105 void* entryr; 106 107 // start pty output at row number 2 (1 is header) 108 int row = 2; 109 // ANSI esc char to point to row/col (indent): 110 // https://tldp.org/HOWTO/Bash-Prompt-HOWTO/x361.html 111 // three digit max rows for winy 112 // start indent col num winx in two digit range 99 113 char indent[strlen("\033[999;99H") + 1]; 114 115 wclear(win); 116 117 int winx, winy; 118 getmaxyx(win, winy, winx); 119 120 if (date_has_entry(dir, dir_size, date)) { 121 // get entry path 122 fpath(dir, dir_size, date, &ppath, sizeof path); 123 if (ppath == NULL) { 124 tracepoint(diary, error_date, "Cannot get file path for entry", date); 125 return; 126 } 127 128 // open master pty 129 if ((fdm = posix_openpt(O_RDWR)) < 0) 130 perror("error on posix_openpt"); 131 132 // unlock slave pty corresponding to master, otherwise, 133 // EIO 5 Input/output error when opening slave fds below 134 if (unlockpt(fdm) < 0) 135 perror("error on unlockpt"); 136 137 if ((pts = ptsname(fdm)) == NULL) 138 perror("error on ptsname"); 139 140 if (strcmp(CONFIG.fmt_cmd, "") == 0) { 141 // no formatting command defined, read and print lines 142 fp = fopen(ppath, "r"); 143 while((c = getc(fp)) != EOF) waddch(win, c); 144 fclose(fp); 145 wrefresh(win); 146 } else { 147 int pipeds[2]; 148 if(pipe(pipeds) == -1) 149 perror("Failed to create pipeds"); 150 151 pid_t pid1, pid2; 152 if ((pid1 = fork()) == 0) { 153 dup2(pipeds[1], STDOUT_FILENO); // re-use stdout in 1 154 close(pipeds[0]); // 1 silently closed by dup2 already 155 156 int max_arg = 10; 157 char* argv[max_arg]; 158 int i = 0; 159 char* tok = strtok(CONFIG.fmt_cmd, " "); 160 while (tok != NULL && i < max_arg-1) { 161 argv[i++] = tok; 162 tok = strtok(NULL, " "); 163 } 164 argv[i++] = ppath; 165 argv[i] = (char*) NULL; 166 167 // use the 'p' style exec to read fmt_cmd from the PATH 168 if (execvp(argv[0], argv) > 0) 169 perror("execvp of --fmt-cmd failed"); 170 exit(0); 171 } 172 173 if ((pid2 = fork()) == 0) { 174 dup2(pipeds[0], STDIN_FILENO); // re-use stdin in 0 175 close(pipeds[1]); // 0 silently closed by dup2 already 176 177 char line[winx]; 178 179 if (CONFIG.no_pty) { 180 while (fgets(line, sizeof line, stdin)) { 181 // print formatted entry from 0 to screen 182 wprintw(win, "%s", line); 183 } 184 wrefresh(win); 185 exit(0); 186 } 187 188 // Read formatted entry line by line. Use fgets, 189 // fseek on stdin/pipe illegal (ESPIPE 29 Illegal seek). 190 // Three digit max rows for preview 999. 191 while (fgets(line, sizeof line, stdin) && row <= winy) { 192 // advance row number in indent 193 sprintf(indent, "\033[%u;%uH", row++, ASIDE_WIDTH + CAL_WIDTH + 1); 194 195 entry_size = strlen(entry); 196 indent_size = strlen(indent); 197 198 // execvp does not return, store all lines in entry buffer 199 entryr = realloc(entry, entry_size + indent_size + strlen(line) + 1); 200 if (entryr == NULL) { 201 perror("failed to realloc entry buffer"); 202 break; 203 } else { 204 // entry ptr already freed by realloc if moved 205 entry = (char*) entryr; 206 // append indent to entry buffer 207 strcat(entry + entry_size, indent); 208 // append line to entry buffer 209 strcat(entry + entry_size + indent_size, line); 210 } 211 } 212 213 // open slave pty 214 if ((fds = open(pts, O_RDWR)) < 0) 215 perror("error opening fds"); 216 217 // print line to pty with ANSI esc chars for indentation 218 char* argv[] = {"echo", "-en", entry, (char*) NULL}; 219 if (execvp(argv[0], argv) > 0) 220 perror("execvp of --fmt-cmd failed"); 221 222 exit(0); 223 } 224 225 close(pipeds[0]); 226 close(pipeds[1]); 227 wait(0); 228 wait(0); 229 } // end no fmt_cmd 230 } else { 231 // date has no entry 232 wrefresh(win); 233 } 234 235 free(entry); 236 } 237 238 /* Writes edit command for 'date' entry to 'rcmd'. '*rcmd' is NULL on error. */ 239 void edit_cmd(const char* dir, size_t dir_size, const struct tm* date, char** rcmd, size_t rcmd_size) { 240 // set the edit command to env editor 241 if (strlen(CONFIG.editor) + 2 > rcmd_size) { 242 tracepoint(diary, error, "Binary path of default editor too long"); 243 *rcmd = NULL; 244 return; 245 } 246 strcpy(*rcmd, CONFIG.editor); 247 strcat(*rcmd, " "); 248 249 // get path of entry 250 char path[100]; 251 char* ppath = path; 252 fpath(dir, dir_size, date, &ppath, sizeof path); 253 254 if (ppath == NULL) { 255 tracepoint(diary, error_date, "Cannot get file path for entry", date); 256 *rcmd = NULL; 257 return; 258 } 259 260 // concatenate editor command with entry path 261 if (strlen(*rcmd) + strlen(path) + 1 > rcmd_size) { 262 tracepoint(diary, error, "Edit command too long"); 263 return; 264 } 265 strcat(*rcmd, path); 266 } 267 268 /* 269 * Finds the date with a diary entry closest to <current>. 270 * Depending on <search_backwards>, it will find the most recent 271 * previous date or the oldest next date with an entry. If no 272 * entry is found within the calendar size, <current> is returned. 273 */ 274 struct tm find_closest_entry(const struct tm current, 275 bool search_backwards, 276 const char* diary_dir, 277 size_t diary_dir_size) { 278 time_t end_time = mktime(&cal_end); 279 time_t start_time = mktime(&cal_start); 280 281 int step = search_backwards ? -1 : +1; 282 283 struct tm it = current; 284 it.tm_mday += step; 285 it.tm_isdst = 0; 286 time_t it_time = mktime(&it); 287 288 for( ; it_time >= start_time && it_time <= end_time; it_time = mktime(&it)) { 289 if (date_has_entry(diary_dir, diary_dir_size, &it)) { 290 return it; 291 } 292 it.tm_mday += step; 293 it.tm_isdst = 0; 294 } 295 296 return current; 297 } 298 299 bool read_config(const char* file_path) { 300 char* expanded_value; 301 char config_file_path[256]; 302 303 expanded_value = expand_path(file_path); 304 strcpy(config_file_path, expanded_value); 305 free(expanded_value); 306 307 // check if config file is readable 308 if( access( config_file_path, R_OK ) != 0 ) { 309 tracepoint(diary, warning_string, "Config file missing or not readable, skipping", config_file_path); 310 return false; 311 } 312 313 char key_buf[80]; 314 char value_buf[80]; 315 char* value_multiword; 316 char line[256]; 317 FILE * pfile; 318 319 // read config file line by line 320 pfile = fopen(config_file_path, "r"); 321 while (fgets(line, sizeof line, pfile)) { 322 // ignore comment lines 323 if (*line == '#' || *line == ';') continue; 324 325 if (sscanf(line, "%79s = %79[^\n]", key_buf, value_buf) == 2) { 326 if (strcmp("dir", key_buf) == 0) { 327 expanded_value = expand_path(value_buf); 328 CONFIG.dir = (char *) malloc(strlen(expanded_value) + 1 * sizeof(char)); 329 strcpy(CONFIG.dir, expanded_value); 330 free(expanded_value); 331 } else if (strcmp("range", key_buf) == 0) { 332 CONFIG.range = atoi(value_buf); 333 } else if (strcmp("weekday", key_buf) == 0) { 334 CONFIG.weekday = atoi(value_buf); 335 } else if (strcmp("fmt", key_buf) == 0) { 336 CONFIG.fmt = (char *) malloc(strlen(value_buf) + 1 * sizeof(char)); 337 strcpy(CONFIG.fmt, value_buf); 338 } else if (strcmp("fmt_cmd", key_buf) == 0) { 339 value_multiword = strstr(line, "="); // multiword value 340 value_multiword++; // strip the "=" 341 value_multiword[strlen(value_multiword) - 1] = '\0'; // strip newline 342 CONFIG.fmt_cmd = (char *) malloc(strlen(value_multiword) + 1 * sizeof(char)); 343 strcpy(CONFIG.fmt_cmd, value_multiword); 344 } else if (strcmp("no_pty", key_buf) == 0) { 345 if (strcmp("1", value_buf) == 0 || strcmp("true", value_buf) == 0) { 346 CONFIG.no_pty = true; 347 } 348 } else if (strcmp("no_mouse", key_buf) == 0) { 349 if (strcmp("1", value_buf) == 0 || strcmp("true", value_buf) == 0) { 350 CONFIG.no_mouse = true; 351 } 352 } else if (strcmp("editor", key_buf) == 0) { 353 CONFIG.editor = (char *) malloc(strlen(value_buf) + 1 * sizeof(char)); 354 strcpy(CONFIG.editor, value_buf); 355 } else if (strcmp("caldav_server", key_buf) == 0) { 356 CONFIG.caldav_server = (char *) malloc(strlen(value_buf) + 1 * sizeof(char)); 357 strcpy(CONFIG.caldav_server, value_buf); 358 } else if (strcmp("caldav_calendar", key_buf) == 0) { 359 CONFIG.caldav_calendar = (char *) malloc(strlen(value_buf) + 1 * sizeof(char)); 360 strcpy(CONFIG.caldav_calendar, value_buf); 361 } else if (strcmp("caldav_username", key_buf) == 0) { 362 CONFIG.caldav_username = (char *) malloc(strlen(value_buf) + 1 * sizeof(char)); 363 strcpy(CONFIG.caldav_username, value_buf); 364 } else if (strcmp("caldav_password", key_buf) == 0) { 365 CONFIG.caldav_password = (char *) malloc(strlen(value_buf) + 1 * sizeof(char)); 366 strcpy(CONFIG.caldav_password, value_buf); 367 } else if (strcmp("oauth_eval_cmd", key_buf) == 0) { 368 CONFIG.oauth_eval_cmd = (char *) malloc(strlen(value_buf) + 1 * sizeof(char)); 369 strcpy(CONFIG.oauth_eval_cmd, value_buf); 370 } 371 } 372 } 373 fclose (pfile); 374 return true; 375 } 376 377 void usage() { 378 printf("Diary v%s: Text-based journaling program\n", DIARY_VERSION); 379 printf("Edit journal entries from the command line.\n"); 380 printf("\n"); 381 printf("Usage : diary [OPTION]... [DIRECTORY]...\n"); 382 printf("\n"); 383 printf("Options:\n"); 384 printf(" -v, --version : Print diary version\n"); 385 printf(" -h, --help : Show diary help text\n"); 386 printf(" -d, --dir DIARY_DIR : Diary storage directory DIARY_DIR\n"); 387 printf(" -e, --editor EDITOR : Editor to open journal files with\n"); 388 printf(" -f, --fmt FMT : Date and file format, change with care\n"); 389 printf(" -F, --fmt-cmd FMT_CMD : Format entry preview with command FMT_CMD\n"); 390 printf(" -p, --no-pty : Result of FMT_CMD not printed to pty but processed within Ncurses\n"); 391 printf(" -m, --no-mouse : Disable mouse, don't listen for mouse events\n"); 392 printf(" -r, --range RANGE : RANGE is the number of years to show before/after today's date\n"); 393 printf(" -w, --weekday DAY : First day of the week, 0 = Sun, 1 = Mon, ..., 6 = Sat\n"); 394 printf("\n"); 395 printf("Full docs and keyboard shortcuts: 'man diary'\n"); 396 printf("or online at <https://code.in0rdr.ch/diary>\n"); 397 } 398 399 int main(int argc, char** argv) { 400 setlocale(LC_ALL, ""); 401 char* env_var; 402 char* config_home; 403 char* config_file_path; 404 chtype atrs; 405 406 // the diary directory defaults to the diary_dir specified in the config file 407 config_home = getenv("XDG_CONFIG_HOME"); 408 if (config_home == NULL) config_home = XDG_CONFIG_HOME_FALLBACK; 409 // concat config home with the file path to the config file 410 config_file_path = (char *) calloc(strlen(config_home) + strlen(CONFIG_FILE_PATH) + 2, sizeof(char)); 411 sprintf(config_file_path, "%s/%s", config_home, CONFIG_FILE_PATH); 412 // read config from config file path 413 read_config(config_file_path); 414 415 // get diary directory from environment 416 env_var = getenv("DIARY_DIR"); 417 if (env_var != NULL) { 418 free(CONFIG.dir); 419 // if available, overwrite the diary directory with the environment variable 420 CONFIG.dir = (char *) calloc(strlen(env_var) + 1, sizeof(char)); 421 strcpy(CONFIG.dir, env_var); 422 } 423 424 // get editor from environment 425 env_var = getenv("EDITOR"); 426 if (env_var != NULL) { 427 // if available, overwrite the editor with the environments EDITOR 428 CONFIG.editor = (char *) calloc(strlen(env_var) + 1, sizeof(char)); 429 strcpy(CONFIG.editor, env_var); 430 } 431 432 // get locale from environment variable LANG 433 // https://www.gnu.org/software/gettext/manual/html_node/Locale-Environment-Variables.html 434 env_var = getenv("LANG"); 435 if (env_var != NULL) { 436 // if available, overwrite the editor with the environments locale 437 #ifdef __GNU_LIBRARY__ 438 // references: locale(5) and util-linux's cal.c 439 // get the base date, 8-digit integer (YYYYMMDD) returned as char * 440 #ifdef _NL_TIME_WEEK_1STDAY 441 unsigned long d = (uintptr_t) nl_langinfo(_NL_TIME_WEEK_1STDAY); 442 // reference: https://sourceware.org/glibc/wiki/Locales 443 // assign a static date value 19971130 (a Sunday) 444 #else 445 unsigned long d = 19971130; 446 #endif 447 struct tm base = { 448 .tm_sec = 0, 449 .tm_min = 0, 450 .tm_hour = 0, 451 .tm_mday = d % 100, 452 .tm_mon = (d / 100) % 100 - 1, 453 .tm_year = d / (100 * 100) - 1900 454 }; 455 mktime(&base); 456 // weekday is base date's day of the week offset by (_NL_TIME_FIRST_WEEKDAY - 1) 457 #ifdef __linux__ 458 CONFIG.weekday = (base.tm_wday + *nl_langinfo(_NL_TIME_FIRST_WEEKDAY) - 1) % 7; 459 #elif defined __MACH__ 460 CFIndex first_day_of_week; 461 CFCalendarRef currentCalendar = CFCalendarCopyCurrent(); 462 first_day_of_week = CFCalendarGetFirstWeekday(currentCalendar); 463 CFRelease(currentCalendar); 464 CONFIG.weekday = (base.tm_wday + first_day_of_week - 1) % 7; 465 #endif 466 #endif 467 } 468 469 int option_char; 470 int option_index = 0; 471 472 // define options, see GETOPT(3) 473 static const struct option long_options[] = { 474 { "version", no_argument, 0, 'v' }, 475 { "help", no_argument, 0, 'h' }, 476 { "dir", required_argument, 0, 'd' }, 477 { "editor", required_argument, 0, 'e' }, 478 { "fmt", required_argument, 0, 'f' }, 479 { "fmt-cmd", required_argument, 0, 'F' }, 480 { "no-pty", no_argument, 0, 'p' }, 481 { "no-mouse", no_argument, 0, 'm' }, 482 { "range", required_argument, 0, 'r' }, 483 { "weekday", required_argument, 0, 'w' }, 484 { 0, 0, 0, 0 } 485 }; 486 487 // read option characters 488 while (1) { 489 option_char = getopt_long(argc, argv, "vhpmd:r:w:f:F:e:", long_options, &option_index); 490 491 if (option_char == -1) { 492 break; 493 } 494 495 switch (option_char) { 496 case 'v': 497 // show program version 498 printf("v%s\n", DIARY_VERSION); 499 return 0; 500 break; 501 case 'h': 502 // show help text 503 // printf("see man(1) diary\n"); 504 usage(); 505 return 0; 506 break; 507 case 'd': 508 free(CONFIG.dir); 509 // set diary directory from option character 510 CONFIG.dir = (char *) calloc(strlen(optarg) + 1, sizeof(char)); 511 strcpy(CONFIG.dir, optarg); 512 break; 513 case 'r': 514 // set year range from option character 515 CONFIG.range = atoi(optarg); 516 break; 517 case 'w': 518 // set first week day from option character 519 CONFIG.weekday = atoi(optarg); 520 break; 521 case 'f': 522 // set date format from option character 523 CONFIG.fmt = (char *) calloc(strlen(optarg) + 1, sizeof(char)); 524 strcpy(CONFIG.fmt, optarg); 525 break; 526 case 'F': 527 free(CONFIG.fmt_cmd); 528 // set formatting command 529 CONFIG.fmt_cmd = (char *) calloc(strlen(optarg) + 1, sizeof(char)); 530 strcpy(CONFIG.fmt_cmd, optarg); 531 break; 532 case 'p': 533 // set no-pty flag 534 CONFIG.no_pty = true; 535 break; 536 case 'm': 537 // set no-mouse flag 538 CONFIG.no_mouse = true; 539 break; 540 case 'e': 541 // set default editor from option character 542 CONFIG.editor = (char *) calloc(strlen(optarg) + 1, sizeof(char)); 543 strcpy(CONFIG.editor, optarg); 544 break; 545 } 546 } 547 548 // Get the diary directory from argument, this takes precedence over env/config. 549 // optind is an external variable set by getopt, see man 3 getopt. The system 550 // initializes this value to 1. If optind >= argc, the DIARY_DIR was not provided. 551 if (optind < argc) { 552 free(CONFIG.dir); 553 // set diary directory from first non-option argv-element, 554 // required for backwarad compatibility with diary <= 0.4 555 CONFIG.dir = (char *) calloc(strlen(argv[optind]) + 1, sizeof(char)); 556 strcpy(CONFIG.dir, argv[optind]); 557 } 558 559 if (CONFIG.dir == NULL) { 560 fprintf(stderr, "The diary directory must be provided as (non-option) arg, `--dir` arg,\n" 561 "or in the DIARY_DIR environment variable, see `diary --help` or DIARY(1)\n"); 562 return 1; 563 } 564 565 // check if that directory exists 566 DIR* diary_dir_ptr = opendir(CONFIG.dir); 567 if (diary_dir_ptr) { 568 // directory exists, continue 569 closedir(diary_dir_ptr); 570 } else if (errno == ENOENT) { 571 fprintf(stderr, "The directory '%s' does not exist\n", CONFIG.dir); 572 free(config_file_path); 573 return 2; 574 } else { 575 fprintf(stderr, "The directory '%s' could not be opened\n", CONFIG.dir); 576 free(config_file_path); 577 return 1; 578 } 579 580 setup_cal_timeframe(); 581 582 initscr(); 583 raw(); 584 curs_set(0); 585 586 WINDOW* header = newwin(1, COLS - CAL_WIDTH - ASIDE_WIDTH, 0, ASIDE_WIDTH + CAL_WIDTH); 587 wattron(header, A_BOLD); 588 update_date(header, &curs_date); 589 WINDOW* wdays = newwin(1, 3 * 7, 0, ASIDE_WIDTH); 590 draw_wdays(wdays); 591 592 WINDOW* aside = newpad((CONFIG.range * 2 + 1) * 12 * MAX_MONTH_HEIGHT, ASIDE_WIDTH); 593 WINDOW* cal = newpad((CONFIG.range * 2 + 1) * 12 * MAX_MONTH_HEIGHT, CAL_WIDTH); 594 // Mouse events under xterm will not be detected correctly 595 // in a window with its keypad bit off 596 keypad(cal, TRUE); 597 draw_calendar(cal, aside, CONFIG.dir, strlen(CONFIG.dir)); 598 599 int ch = 0, conf_ch = 0; 600 int pad_pos = 0; 601 int syear = 0, smonth = 0, sday = 0; 602 char ics_filepath; 603 char* expanded_ics_filepath; 604 struct tm new_date; 605 int prev_width = COLS - ASIDE_WIDTH - CAL_WIDTH; 606 int prev_height = LINES - 1; 607 size_t diary_dir_size = strlen(CONFIG.dir); 608 609 bool mv_valid = go_to(cal, aside, mktime(&today), &pad_pos, &curs_date, &cal_start, &cal_end); 610 // mark current day 611 atrs = winch(cal) & A_ATTRIBUTES; 612 wchgat(cal, 2, atrs | A_UNDERLINE, 0, NULL); 613 prefresh(cal, pad_pos, 0, 1, ASIDE_WIDTH, LINES - 1, ASIDE_WIDTH + CAL_WIDTH); 614 615 WINDOW* prev = newwin(prev_height, prev_width, 1, ASIDE_WIDTH + CAL_WIDTH); 616 display_entry(CONFIG.dir, diary_dir_size, &today, prev, prev_width); 617 618 mmask_t oldmask; 619 MEVENT event; 620 621 if (!CONFIG.no_mouse) { 622 // listen for mouse events if not disabled explicitly 623 mousemask(ALL_MOUSE_EVENTS, &oldmask); 624 } 625 626 do { 627 ch = wgetch(cal); 628 // new_date represents the desired date the user wants to go_to(), 629 // which may not be a feasible date at all 630 new_date = curs_date; 631 new_date.tm_isdst = 0; 632 char ecmd[150]; 633 char* pecmd = ecmd; 634 char pth[100]; 635 char* ppth = pth; 636 char dstr[16]; 637 mktime(&new_date); 638 edit_cmd(CONFIG.dir, diary_dir_size, &new_date, &pecmd, sizeof ecmd); 639 640 switch(ch) { 641 // redraw on win resize 642 case KEY_RESIZE: 643 mv_valid = go_to(cal, aside, mktime(&new_date), &pad_pos, &curs_date, &cal_start, &cal_end); 644 break; 645 // basic movements 646 case 'j': 647 case KEY_DOWN: 648 new_date.tm_mday += 7; 649 mv_valid = go_to(cal, aside, mktime(&new_date), &pad_pos, &curs_date, &cal_start, &cal_end); 650 break; 651 case 'k': 652 case KEY_UP: 653 new_date.tm_mday -= 7; 654 mv_valid = go_to(cal, aside, mktime(&new_date), &pad_pos, &curs_date, &cal_start, &cal_end); 655 break; 656 case 'l': 657 case KEY_RIGHT: 658 new_date.tm_mday++; 659 mv_valid = go_to(cal, aside, mktime(&new_date), &pad_pos, &curs_date, &cal_start, &cal_end); 660 break; 661 case 'h': 662 case KEY_LEFT: 663 new_date.tm_mday--; 664 mv_valid = go_to(cal, aside, mktime(&new_date), &pad_pos, &curs_date, &cal_start, &cal_end); 665 break; 666 667 // jump to top/bottom of page 668 case 'g': 669 new_date = find_closest_entry(cal_start, false, CONFIG.dir, diary_dir_size); 670 mv_valid = go_to(cal, aside, mktime(&new_date), &pad_pos, &curs_date, &cal_start, &cal_end); 671 break; 672 case 'G': 673 new_date = find_closest_entry(cal_end, true, CONFIG.dir, diary_dir_size); 674 mv_valid = go_to(cal, aside, mktime(&new_date), &pad_pos, &curs_date, &cal_start, &cal_end); 675 break; 676 677 // jump backward/forward by a month 678 case 'K': 679 if (new_date.tm_mday == 1) 680 new_date.tm_mon--; 681 new_date.tm_mday = 1; 682 mv_valid = go_to(cal, aside, mktime(&new_date), &pad_pos, &curs_date, &cal_start, &cal_end); 683 break; 684 case 'J': 685 new_date.tm_mon++; 686 new_date.tm_mday = 1; 687 mv_valid = go_to(cal, aside, mktime(&new_date), &pad_pos, &curs_date, &cal_start, &cal_end); 688 break; 689 690 // find specific date 691 case 'f': 692 wclear(header); 693 curs_set(2); 694 mvwprintw(header, 0, 0, "Go to date [YYYY-MM-DD]: "); 695 if (wscanw(header, "%4i-%2i-%2i", &syear, &smonth, &sday) == 3) { 696 // struct tm.tm_year: years since 1900 697 new_date.tm_year = syear - 1900; 698 // struct tm.tm_mon in range [0, 11] 699 new_date.tm_mon = smonth - 1; 700 new_date.tm_mday = sday; 701 mv_valid = go_to(cal, aside, mktime(&new_date), &pad_pos, &curs_date, &cal_start, &cal_end); 702 } 703 curs_set(0); 704 break; 705 // jump to specific date using the mouse 706 case KEY_MOUSE: 707 if(getmouse(&event) == OK) { 708 if (wenclose(cal, event.y, event.x)) { 709 if(event.bstate & (BUTTON1_PRESSED|BUTTON1_CLICKED)) { 710 // regular left-mouse button click or tap on a touchpad: 711 // BUTTON1_PRESSED detects touch events (touch pads) 712 // BUTTON1_CLICKED detects click events (mouse) 713 int cy, cx; 714 getyx(cal, cy, cx); 715 int pad_cy = cy - pad_pos + 1; 716 int pad_cx = cx + ASIDE_WIDTH; 717 718 int diff_weeks = abs(pad_cy - event.y); 719 int diff_wdays = abs((pad_cx - event.x) / 3); 720 721 int diff_days = 0; 722 if (pad_cy > event.y) { 723 // current position cy is more recent, jump backward by diff_days 724 diff_days -= diff_weeks * 7; 725 } else { 726 // new y event is more recent, jump forward by diff_days 727 diff_days = diff_weeks * 7; 728 } 729 730 if (cx > event.x) { 731 // jump backwards, mouse click was before current position 732 diff_days -= diff_wdays; 733 } else { 734 // jump forward, mouse click was after current position 735 diff_days += diff_wdays; 736 } 737 738 curs_date.tm_mday += diff_days; 739 time_t new_date = mktime(&curs_date); 740 mv_valid = go_to(cal, aside, new_date, &pad_pos, &curs_date, &cal_start, &cal_end); 741 742 if (event.bstate & (BUTTON1_DOUBLE_CLICKED)) { 743 // unset previous click events 744 event.bstate &= ~BUTTON1_DOUBLE_CLICKED; 745 event.bstate &= ~BUTTON1_PRESSED; 746 event.bstate &= ~BUTTON1_CLICKED; 747 // simulate edit char to enter edit mode 748 ungetch('e'); 749 } 750 } 751 if (event.bstate & (BUTTON1_DOUBLE_CLICKED)) { 752 // double-click left-mouse button or double tap on a touchpad 753 // simulate single-click to select the new date 754 event.bstate |= BUTTON1_PRESSED; 755 ungetmouse(&event); 756 } 757 } 758 } 759 break; 760 761 // today shortcut 762 case 't': 763 mv_valid = go_to(cal, aside, mktime(&today), &pad_pos, &curs_date, &cal_start, &cal_end); 764 break; 765 // delete entry 766 case 'd': 767 case 'x': 768 if (date_has_entry(CONFIG.dir, diary_dir_size, &curs_date)) { 769 // get file path of entry and delete entry 770 fpath(CONFIG.dir, diary_dir_size, &curs_date, &ppth, sizeof pth); 771 if (ppth == NULL) { 772 tracepoint(diary, error_date, "Cannot get file path for entry", &curs_date); 773 break; 774 } 775 776 // prepare header for confirmation dialogue 777 wclear(header); 778 curs_set(2); 779 noecho(); 780 781 // ask for confirmation 782 strftime(dstr, sizeof dstr, CONFIG.fmt, &curs_date); 783 mvwprintw(header, 0, 0, "Delete entry '%s'? [Y/n] ", dstr); 784 bool conf = false; 785 while (!conf) { 786 conf_ch = wgetch(header); 787 if (conf_ch == 'y' || conf_ch == 'Y' || conf_ch == '\n') { 788 if (unlink(pth) != -1) { 789 // file successfully deleted, remove entry highlight 790 atrs = winch(cal) & A_ATTRIBUTES; 791 wchgat(cal, 2, atrs & ~A_BOLD, 0, NULL); 792 prefresh(cal, pad_pos, 0, 1, ASIDE_WIDTH, 793 LINES - 1, ASIDE_WIDTH + CAL_WIDTH); 794 } 795 } else if (conf_ch == 27 || conf_ch == 'n') { 796 update_date(header, &curs_date); 797 } 798 break; 799 } 800 801 echo(); 802 curs_set(0); 803 } 804 break; 805 // edit/create a diary entry 806 case 'e': 807 case '\n': 808 if (pecmd == NULL) { 809 tracepoint(diary, error, "Error retrieving edit command"); 810 break; 811 } 812 813 curs_set(1); 814 815 if (!CONFIG.no_mouse) { 816 // listen for mouse events if not disabled explicitly 817 mousemask(oldmask, NULL); 818 } 819 820 int ret = system(ecmd); 821 if (ret == -1) { 822 perror("Failure while running edit command in main()"); 823 } 824 825 if (!CONFIG.no_mouse) { 826 mousemask(ALL_MOUSE_EVENTS, &oldmask); 827 } 828 829 curs_set(0); 830 keypad(cal, TRUE); 831 832 // mark newly created entry 833 if (date_has_entry(CONFIG.dir, diary_dir_size, &curs_date)) { 834 atrs = winch(cal) & A_ATTRIBUTES; 835 wchgat(cal, 2, atrs | A_BOLD, 0, NULL); 836 837 // refresh the calendar to add highlighting 838 prefresh(cal, pad_pos, 0, 1, ASIDE_WIDTH, 839 LINES - 1, ASIDE_WIDTH + CAL_WIDTH); 840 } 841 break; 842 // Move to the previous diary entry 843 case 'N': 844 new_date = find_closest_entry(new_date, true, CONFIG.dir, diary_dir_size); 845 mv_valid = go_to(cal, aside, mktime(&new_date), &pad_pos, &curs_date, &cal_start, &cal_end); 846 break; 847 // Move to the next diary entry 848 case 'n': 849 new_date = find_closest_entry(new_date, false, CONFIG.dir, diary_dir_size); 850 mv_valid = go_to(cal, aside, mktime(&new_date), &pad_pos, &curs_date, &cal_start, &cal_end); 851 break; 852 // Sync entry with CalDAV server. 853 // Show confirmation dialogue before overwriting local files 854 case 's': 855 caldav_sync(&curs_date, header, cal, pad_pos, CONFIG.dir, diary_dir_size, true); 856 break; 857 // Sync entire month with CalDAV server; 858 case 'S': 859 // end at last day of month 860 new_date.tm_mon++; 861 new_date.tm_mday = 0; 862 time_t end_of_month = mktime(&new_date); 863 864 // start at first day of month 865 new_date.tm_mday = 1; 866 time_t new_datetime = mktime(&new_date); 867 868 // reset confirmation char from previous sync attempts 869 conf_ch = 0; 870 for( ; new_datetime <= end_of_month; new_datetime = mktime(&new_date)) { 871 go_to(cal, aside, mktime(&new_date), &pad_pos, &curs_date, &cal_start, &cal_end); 872 update_date(header, &curs_date); 873 if (conf_ch == -1) { 874 // sync error 875 break; 876 } else if (conf_ch == 'c') { 877 // cancel all 878 break; 879 } else if (conf_ch == 'a') { 880 // yes to (a)ll, don't read returned conf_ch, since this will be reset to 0 881 caldav_sync(&new_date, header, cal, pad_pos, CONFIG.dir, diary_dir_size, false); 882 } else { 883 // show confirmation dialogue before overwriting local files 884 conf_ch = caldav_sync(&new_date, header, cal, pad_pos, CONFIG.dir, diary_dir_size, true); 885 } 886 display_entry(CONFIG.dir, diary_dir_size, &curs_date, prev, prev_width); 887 new_date.tm_mday++; 888 } 889 break; 890 // import from ics file 891 case 'i': 892 wclear(header); 893 curs_set(2); 894 mvwprintw(header, 0, 0, "Import from file: "); 895 if (wscanw(header, "%256s", &ics_filepath) == 1) { 896 expanded_ics_filepath = expand_path(&ics_filepath); 897 ics_import(expanded_ics_filepath, header, cal, aside, &pad_pos, &curs_date, &cal_start, &cal_end); 898 free(expanded_ics_filepath); 899 } 900 901 curs_set(0); 902 echo(); 903 break; 904 // export to ics file 905 case 'E': 906 wclear(header); 907 curs_set(2); 908 mvwprintw(header, 0, 0, "Export to file: "); 909 if (wscanw(header, "%256s", &ics_filepath) == 1) { 910 expanded_ics_filepath = expand_path(&ics_filepath); 911 ics_export(expanded_ics_filepath, header, cal, aside, &pad_pos, &curs_date, &cal_start, &cal_end); 912 free(expanded_ics_filepath); 913 } 914 915 curs_set(0); 916 echo(); 917 break; 918 } 919 920 if (mv_valid) { 921 update_date(header, &curs_date); 922 923 // adjust prev and header width (if terminal was resized in the mean time) 924 prev_width = COLS - ASIDE_WIDTH - CAL_WIDTH; 925 wresize(prev, prev_height, prev_width); 926 wresize(header, 1, prev_width); 927 928 // read the diary 929 if (ch != 'q') 930 display_entry(CONFIG.dir, diary_dir_size, &curs_date, prev, prev_width); 931 } 932 } while (ch != 'q'); 933 934 free(config_file_path); 935 free(CONFIG.dir); 936 endwin(); 937 int ret = system("clear"); 938 if (ret == -1) { 939 perror("Failure while running clear command in main()"); 940 } 941 return 0; 942 }