diary

Text-based journaling program
git clone https://git.in0rdr.ch/diary.git
Log | Files | Refs | Pull requests | README | LICENSE

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 }