/* * Calcurse - text-based organizer * * Copyright (c) 2004-2020 calcurse Development Team * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * - Redistributions of source code must retain the above * copyright notice, this list of conditions and the * following disclaimer. * * - Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the * following disclaimer in the documentation and/or other * materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * Send your feedback or comments to : misc@calcurse.org * Calcurse home page : http://calcurse.org * */ #include #include #include #include #include #include #include #include #include #include #include "calcurse.h" #include "sha1.h" struct ht_keybindings_s { const char *label; enum key key; HTABLE_ENTRY(ht_keybindings_s); }; static void load_keys_ht_getkey(struct ht_keybindings_s *, const char **, int *); static int load_keys_ht_compare(struct ht_keybindings_s *, struct ht_keybindings_s *); #define HSIZE 256 HTABLE_HEAD(ht_keybindings, HSIZE, ht_keybindings_s); HTABLE_PROTOTYPE(ht_keybindings, ht_keybindings_s) HTABLE_GENERATE(ht_keybindings, ht_keybindings_s, load_keys_ht_getkey, load_keys_ht_compare) static int modified = 0; static char apts_sha1[SHA1_DIGESTLEN * 2 + 1]; static char todo_sha1[SHA1_DIGESTLEN * 2 + 1]; /* Ask user for a file name to export data to. */ static FILE *get_export_stream(enum export_type type) { FILE *stream; char *home, *stream_name; const char *question = _("Choose the file used to export calcurse data:"); const char *wrong_name = _("The file cannot be accessed, please enter another file name."); const char *press_enter = _("Press [ENTER] to continue."); const char *file_ext[IO_EXPORT_NBTYPES] = { "ical", "txt" }; stream = NULL; if ((home = getenv("HOME")) != NULL) asprintf(&stream_name, "%s/calcurse.%s", home, file_ext[type]); else asprintf(&stream_name, "%s/calcurse.%s", get_tempdir(), file_ext[type]); while (stream == NULL) { status_mesg(question, ""); if (updatestring(win[STA].p, &stream_name, 0, 1)) { mem_free(stream_name); return NULL; } stream = fopen(stream_name, "w"); if (stream == NULL) { status_mesg(wrong_name, press_enter); keys_wait_for_any_key(win[KEY].p); } } mem_free(stream_name); return stream; } /* Append a line to a file. */ unsigned io_fprintln(const char *fname, const char *fmt, ...) { FILE *fp; va_list ap; char *buf; int ret; fp = fopen(fname, "a"); RETVAL_IF(!fp, 0, _("Failed to open \"%s\", - %s\n"), fname, strerror(errno)); va_start(ap, fmt); ret = vasprintf(&buf, fmt, ap); RETVAL_IF(ret < 0, 0, _("Failed to build message\n")); va_end(ap); ret = fprintf(fp, "%s", buf); RETVAL_IF(ret < 0, 0, _("Failed to print message \"%s\"\n"), buf); ret = fclose(fp); RETVAL_IF(ret != 0, 0, _("Failed to close \"%s\" - %s\n"), fname, strerror(errno)); mem_free(buf); return 1; } /* * Initialization of data paths. The cfile argument is the variable * which contains the calendar file. If none is given, then the default * one (~/.local/share/calcurse/apts) is taken. If the one given does not exist, * it is created. * The datadir argument can be used to specify an alternative data root dir. * The confdir argument can be used to specify an alternative configuration dir. * If ~/.calcurse exists, it will be used instead for backward compatibility. */ void io_init(const char *cfile, const char *datadir, const char *confdir) { char* home_dir = getenv("HOME"); char* legacy_dir = NULL; if (home_dir) { asprintf(&legacy_dir, "%s%s", home_dir, "/" DIR_NAME_LEGACY); if (!io_dir_exists(legacy_dir)) { mem_free(legacy_dir); legacy_dir = NULL; } } if (datadir) asprintf(&path_ddir, "%s%s", datadir, "/"); else if (legacy_dir) path_ddir = mem_strdup(legacy_dir); else if ((path_ddir = getenv("XDG_DATA_HOME"))) asprintf(&path_ddir, "%s%s", path_ddir, "/" DIR_NAME); else if (home_dir) asprintf(&path_ddir, "%s%s", home_dir, "/.local/share/" DIR_NAME); else path_ddir = mem_strdup("./." DIR_NAME); if (confdir) asprintf(&path_cdir, "%s%s", confdir, "/"); else if (datadir) path_cdir = mem_strdup(path_ddir); else if (legacy_dir) path_cdir = mem_strdup(legacy_dir); else if ((path_cdir = getenv("XDG_CONFIG_HOME"))) asprintf(&path_cdir, "%s%s", path_cdir, "/" DIR_NAME); else if (home_dir) asprintf(&path_cdir, "%s%s", home_dir, "/.config/" DIR_NAME); else path_cdir = mem_strdup("./." DIR_NAME); if (legacy_dir) mem_free(legacy_dir); /* Data files */ if (cfile) { path_apts = mem_strdup(cfile); EXIT_IF(!io_file_exists(path_apts), _("%s does not exist"), path_apts); } else { asprintf(&path_apts, "%s%s", path_ddir, APTS_PATH_NAME); } asprintf(&path_todo, "%s%s", path_ddir, TODO_PATH_NAME); asprintf(&path_cpid, "%s%s", path_ddir, CPID_PATH_NAME); asprintf(&path_dpid, "%s%s", path_ddir, DPID_PATH_NAME); asprintf(&path_notes, "%s%s", path_ddir, NOTES_DIR_NAME); asprintf(&path_dmon_log, "%s%s", path_ddir, DLOG_PATH_NAME); /* Configuration files */ asprintf(&path_conf, "%s%s", path_cdir, CONF_PATH_NAME); asprintf(&path_keys, "%s%s", path_cdir, KEYS_PATH_NAME); asprintf(&path_hooks, "%s%s", path_cdir, HOOKS_DIR_NAME); } void io_extract_data(char *dst_data, const char *org, int len) { int i; for (; *org == ' ' || *org == '\t'; org++) ; for (i = 0; i < len - 1; i++) { if (*org == '\n' || *org == '\0') break; *dst_data++ = *org++; } *dst_data = '\0'; } static pthread_mutex_t io_mutex = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t io_periodic_save_mutex = PTHREAD_MUTEX_INITIALIZER; static void io_mutex_lock(void) { pthread_mutex_lock(&io_mutex); } static void io_mutex_unlock(void) { pthread_mutex_unlock(&io_mutex); } /* Print all appointments and events to stdout. */ void io_dump_apts(const char *fmt_apt, const char *fmt_rapt, const char *fmt_ev, const char *fmt_rev) { llist_item_t *i; LLIST_FOREACH(&recur_elist, i) { struct recur_event *rev = LLIST_GET_DATA(i); time_t day = update_time_in_date(rev->day, 0, 0); print_recur_event(fmt_rev, day, rev); } LLIST_TS_FOREACH(&recur_alist_p, i) { struct recur_apoint *rapt = LLIST_GET_DATA(i); time_t day = update_time_in_date(rapt->start, 0, 0); print_recur_apoint(fmt_rapt, day, rapt->start, rapt); } LLIST_TS_FOREACH(&alist_p, i) { struct apoint *apt = LLIST_TS_GET_DATA(i); time_t day = update_time_in_date(apt->start, 0, 0); print_apoint(fmt_apt, day, apt); } LLIST_FOREACH(&eventlist, i) { struct event *ev = LLIST_TS_GET_DATA(i); time_t day = update_time_in_date(ev->day, 0, 0); print_event(fmt_ev, day, ev); } } /* * Save the apts data file, which contains the * appointments first, and then the events. * Recursive items are written first. */ unsigned io_save_apts(const char *aptsfile) { llist_item_t *i; FILE *fp; if (aptsfile) { if (read_only) return 1; if ((fp = fopen(aptsfile, "w")) == NULL) return 0; } else { fp = stdout; } recur_save_data(fp); if (ui_mode == UI_CURSES) LLIST_TS_LOCK(&alist_p); LLIST_TS_FOREACH(&alist_p, i) { struct apoint *apt = LLIST_TS_GET_DATA(i); apoint_write(apt, fp); } if (ui_mode == UI_CURSES) LLIST_TS_UNLOCK(&alist_p); LLIST_FOREACH(&eventlist, i) { struct event *ev = LLIST_TS_GET_DATA(i); event_write(ev, fp); } if (aptsfile) file_close(fp, __FILE_POS__); return 1; } /* Print all todo items to stdout. */ void io_dump_todo(const char *fmt_todo) { llist_item_t *i; LLIST_FOREACH(&todolist, i) { struct todo *todo = LLIST_TS_GET_DATA(i); print_todo(fmt_todo, todo); } } /* Save the todo data file. */ unsigned io_save_todo(const char *todofile) { llist_item_t *i; FILE *fp; if (todofile) { if (read_only) return 1; if ((fp = fopen(todofile, "w")) == NULL) return 0; } else { fp = stdout; } LLIST_FOREACH(&todolist, i) { struct todo *todo = LLIST_TS_GET_DATA(i); todo_write(todo, fp); } if (todofile) file_close(fp, __FILE_POS__); return 1; } /* Save user-defined keys */ unsigned io_save_keys(void) { FILE *fp; if (read_only) return 1; if ((fp = fopen(path_keys, "w")) == NULL) return 0; keys_save_bindings(fp); file_close(fp, __FILE_POS__); return 1; } static int io_compute_hash(const char *path, char *buf) { FILE *fp = fopen(path, "r"); if (!fp) return 0; sha1_stream(fp, buf); fclose(fp); return 1; } /* A merge implies a save operation and must be followed by reload of data. */ static void io_merge_data(void) { char *path_apts_new, *path_todo_new; const char *new_ext = ".new"; asprintf(&path_apts_new, "%s%s", path_apts, new_ext); asprintf(&path_todo_new, "%s%s", path_todo, new_ext); io_save_apts(path_apts_new); io_save_todo(path_todo_new); /* * We do not directly write to the data files here; however, the * external merge tool might incorporate changes from the new file into * the main data files. */ run_hook("pre-save"); if (!io_files_equal(path_apts, path_apts_new)) { const char *arg_apts[] = { conf.mergetool, path_apts, path_apts_new, NULL }; wins_launch_external(arg_apts); } if (!io_files_equal(path_todo, path_todo_new)) { const char *arg_todo[] = { conf.mergetool, path_todo, path_todo_new, NULL }; wins_launch_external(arg_todo); } mem_free(path_apts_new); mem_free(path_todo_new); /* * We do not directly write to the data files here; however, the * external merge tool will likely have incorporated changes from the * new file into the main data files at this point. */ run_hook("post-save"); /* * The user has merged, so override the modified flag * (and follow up with reload of the data files). */ io_unset_modified(); } /* For the return values, see io_save_cal() below. */ static int resolve_save_conflict(void) { char *msg_um_asktype = NULL; const char *msg_um_prefix = _("Data files have changed and will be overwritten:"); const char *msg_um_overwrite = _("(c)ontinue"); const char *msg_um_merge = _("(m)erge"); const char *msg_um_keep = _("c(a)ncel"); const char *msg_um_choice = _("[cma]"); int ret = IO_SAVE_CANCEL; asprintf(&msg_um_asktype, "%s %s, %s, %s", msg_um_prefix, msg_um_overwrite, msg_um_merge, msg_um_keep); switch (status_ask_choice(msg_um_asktype, msg_um_choice, 3)) { case 1: ret = IO_SAVE_CTINUE; break; case 2: io_merge_data(); io_load_data(NULL, FORCE); ret = IO_SAVE_RELOAD; break; case 3: /* FALLTHROUGH */ default: ret = IO_SAVE_CANCEL; } mem_free(msg_um_asktype); return ret; } /* * Return codes for new_data() and io_load_data(). * Note that they are file internal. */ #define NONEW 0 #define APTS (1 << 0) #define TODO (1 << 1) #define APTS_TODO APTS | TODO #define NOKNOW -1 static int new_data() { char sha1_new[SHA1_DIGESTLEN * 2 + 1]; int ret = NONEW; if (io_compute_hash(path_apts, sha1_new)) { if (strncmp(sha1_new, apts_sha1, SHA1_DIGESTLEN * 2) != 0) { ret |= APTS; } } else { ret = NOKNOW; goto exit; } if (io_compute_hash(path_todo, sha1_new)) { if (strncmp(sha1_new, todo_sha1, SHA1_DIGESTLEN * 2) != 0) { ret |= TODO; } } else { ret = NOKNOW; goto exit; } exit: return ret; } /* * Save the calendar data. * The return value tells how a possible save conflict should be/was resolved: * IO_SAVE_CTINUE: continue save operation and overwrite the data files * IO_SAVE_RELOAD: cancel save operation (data files changed and reloaded) * IO_SAVE_CANCEL: cancel save operation (user's decision, keep data files, no reload) * IO_SAVE_NOOP: cancel save operation (nothing has changed) * IO_SAVE_ERROR: cannot access data */ int io_save_cal(enum save_type s_t) { int ret, new; if (read_only) return IO_SAVE_CANCEL; io_mutex_lock(); if ((new = new_data()) == NOKNOW) { ret = IO_SAVE_ERROR; goto cleanup; } if (new) { /* New data */ if (s_t == periodic) { ret = IO_SAVE_CANCEL; goto cleanup; } /* Interactively decide what to do. */ if ((ret = resolve_save_conflict())) goto cleanup; } else /* No new data */ if (!io_get_modified()) { ret = IO_SAVE_NOOP; goto cleanup; } ret = IO_SAVE_CTINUE; run_hook("pre-save"); if (io_save_todo(path_todo) && io_save_apts(path_apts)) { io_compute_hash(path_apts, apts_sha1); io_compute_hash(path_todo, todo_sha1); io_unset_modified(); } else ret = IO_SAVE_ERROR; run_hook("post-save"); cleanup: io_mutex_unlock(); return ret; } static void io_load_error(const char *filename, unsigned line, const char *mesg) { EXIT("%s:%u: %s", filename, line, mesg); } /* * Check what type of data is written in the appointment file, * and then load either: a new appointment, a new event, or a new * recursive item (which can also be either an event or an appointment). */ void io_load_app(struct item_filter *filter) { FILE *data_file; int c, is_appointment, is_event, is_recursive; struct tm start, end, until, lt; struct rpt rpt; time_t t; int id = 0; char type, state = 0L; char note[MAX_NOTESIZ + 1], *notep; unsigned line = 0; char *scan_error; t = time(NULL); localtime_r(&t, <); start = end = until = lt; data_file = fopen(path_apts, "r"); EXIT_IF(data_file == NULL, _("failed to open appointment file")); sha1_stream(data_file, apts_sha1); rewind(data_file); for (;;) { is_appointment = is_event = is_recursive = 0; line++; scan_error = NULL; c = getc(data_file); if (c == EOF) break; ungetc(c, data_file); /* Read the date first: it is common to both events * and appointments. */ if (fscanf(data_file, "%d / %d / %d ", &start.tm_mon, &start.tm_mday, &start.tm_year) != 3) io_load_error(path_apts, line, _("syntax error in the item date")); /* Read the next character : if it is an '@' then we have * an appointment, else if it is an '[' we have en event. */ c = getc(data_file); if (c == '@') is_appointment = 1; else if (c == '[') is_event = 1; else io_load_error(path_apts, line, _("no event nor appointment found")); /* Read the remaining informations. */ if (is_appointment) { if (fscanf (data_file, " %d : %d -> %d / %d / %d @ %d : %d ", &start.tm_hour, &start.tm_min, &end.tm_mon, &end.tm_mday, &end.tm_year, &end.tm_hour, &end.tm_min) != 7) io_load_error(path_apts, line, _("syntax error in item time or duration")); } else if (is_event) { if (fscanf(data_file, " %d ", &id) != 1 || getc(data_file) != ']') io_load_error(path_apts, line, _("syntax error in item identifier")); while ((c = getc(data_file)) == ' ') ; ungetc(c, data_file); } else { io_load_error(path_apts, line, _("wrong format in the appointment or event")); /* NOTREACHED */ } /* Check if we have a recursive item. */ c = getc(data_file); if (c == '{') { is_recursive = 1; if (fscanf(data_file, " %d%c ", &rpt.freq, &type) != 2) io_load_error(path_apts, line, _("syntax error in item repetition")); else rpt.type = recur_char2def(type); c = getc(data_file); /* Optional until date */ if (c == '-' && getc(data_file) == '>') { if (fscanf (data_file, " %d / %d / %d ", &until.tm_mon, &until.tm_mday, &until.tm_year) != 3) io_load_error(path_apts, line, _("syntax error in until date")); if (!check_date(until.tm_year, until.tm_mon, until.tm_mday)) io_load_error(path_apts, line, _("until date error")); until.tm_hour = 0; until.tm_min = 0; until.tm_sec = 0; until.tm_isdst = -1; until.tm_year -= 1900; until.tm_mon--; rpt.until = mktime(&until); c = getc(data_file); } else rpt.until = 0; /* Optional bymonthday list */ if (c == 'd') { if (rpt.type == RECUR_WEEKLY) io_load_error(path_apts, line, _("BYMONTHDAY illegal with WEEKLY")); ungetc(c, data_file); recur_bymonthday(&rpt.bymonthday, data_file); c = getc(data_file); } else LLIST_INIT(&rpt.bymonthday); /* Optional bywday list */ if (c == 'w') { ungetc(c, data_file); recur_bywday(rpt.type, &rpt.bywday, data_file); c = getc(data_file); } else LLIST_INIT(&rpt.bywday); /* Optional bymonth list */ if (c == 'm') { ungetc(c, data_file); recur_bymonth(&rpt.bymonth, data_file); c = getc(data_file); } else LLIST_INIT(&rpt.bymonth); /* Optional exception dates */ if (c == '!') { ungetc(c, data_file); recur_exc_scan(&rpt.exc, data_file); c = getc(data_file); } else LLIST_INIT(&rpt.exc); /* End of recurrence rule */ if (c != '}') io_load_error(path_apts, line, _("missing end of recurrence")); while ((c = getc(data_file)) == ' ') ; } /* Check if a note is attached to the item. */ if (c == '>') { note_read(note, data_file); c = getc(data_file); notep = note; } else notep = NULL; /* * Last: read the item description and load it into its * corresponding linked list, depending on the item type. */ if (is_appointment) { if (c == '!') state |= APOINT_NOTIFY; else if (c == '|') state = 0L; else io_load_error(path_apts, line, _("syntax error in item state")); if (is_recursive) scan_error = recur_apoint_scan(data_file, start, end, state, notep, filter, &rpt); else scan_error = apoint_scan(data_file, start, end, state, notep, filter); } else if (is_event) { ungetc(c, data_file); if (is_recursive) scan_error = recur_event_scan(data_file, start, id, notep, filter, &rpt); else scan_error = event_scan(data_file, start, id, notep, filter); } else { io_load_error(path_apts, line, _("wrong format in the appointment or event")); /* NOTREACHED */ } if (scan_error) io_load_error(path_apts, line, scan_error); } file_close(data_file, __FILE_POS__); } /* Load the todo data */ void io_load_todo(struct item_filter *filter) { FILE *data_file; char *newline; int c, id, completed, cond; char buf[BUFSIZ], e_todo[BUFSIZ], note[MAX_NOTESIZ + 1]; unsigned line = 0; data_file = fopen(path_todo, "r"); EXIT_IF(data_file == NULL, _("failed to open todo file")); sha1_stream(data_file, todo_sha1); rewind(data_file); for (;;) { line++; c = getc(data_file); if (c == EOF) { break; } else if (c == '[') { /* new style with id */ c = getc(data_file); if (c == '-') { completed = 1; } else { completed = 0; ungetc(c, data_file); } if (fscanf(data_file, " %d ", &id) != 1 || getc(data_file) != ']') io_load_error(path_todo, line, _("syntax error in item identifier")); while ((c = getc(data_file)) == ' ') ; ungetc(c, data_file); } else { id = 9; completed = 0; ungetc(c, data_file); } /* Now read the attached note, if any. */ c = getc(data_file); if (c == '>') { note_read(note, data_file); } else { note[0] = '\0'; ungetc(c, data_file); } /* Then read todo description. */ if (!fgets(buf, sizeof buf, data_file)) buf[0] = '\0'; newline = strchr(buf, '\n'); if (newline) *newline = '\0'; io_extract_data(e_todo, buf, sizeof buf); /* Filter item. */ struct todo *todo = NULL; if (filter) { cond = ( !(filter->type_mask & TYPE_MASK_TODO) || (filter->regex && regexec(filter->regex, e_todo, 0, 0, 0)) || (filter->priority && id != filter->priority) || (filter->completed && !completed) || (filter->uncompleted && completed) ); if (filter->hash) { todo = todo_add(e_todo, id, completed, note); char *hash = todo_hash(todo); cond = cond || !hash_matches(filter->hash, hash); mem_free(hash); } if ((!filter->invert && cond) || (filter->invert && !cond)) { if (filter->hash) todo_delete(todo); continue; } } if (!todo) todo = todo_add(e_todo, id, completed, note); } file_close(data_file, __FILE_POS__); } /* * Load appointments and todo items. * Unless told otherwise, the function will only load a file that has changed * since last saved or loaded. The new_data() return code is passed on when * force is false. When force is true (FORCE), the return code is of no use. */ int io_load_data(struct item_filter *filter, int force) { run_hook("pre-load"); if (force) force = APTS_TODO; else force = new_data(); if (force == NOKNOW) goto exit; if (force & APTS) { apoint_llist_free(); event_llist_free(); recur_apoint_llist_free(); recur_event_llist_free(); apoint_llist_init(); event_llist_init(); recur_apoint_llist_init(); recur_event_llist_init(); io_load_app(filter); } if (force & TODO) { todo_free_list(); todo_init_list(); io_load_todo(filter); } io_unset_modified(); exit: run_hook("post-load"); return force; } /* * The return codes reflect the user choice in case of unsaved in-memory changes. */ int io_reload_data(void) { char *msg_um_asktype = NULL; int load = NOFORCE; int ret = IO_RELOAD_LOAD; io_mutex_lock(); if (io_get_modified()) { const char *msg_um_prefix = _("Screen data have changed and will be lost:"); const char *msg_um_discard = _("(c)ontinue"); const char *msg_um_merge = _("(m)erge"); const char *msg_um_keep = _("c(a)ncel"); const char *msg_um_choice = _("[cma]"); asprintf(&msg_um_asktype, "%s %s, %s, %s", msg_um_prefix, msg_um_discard, msg_um_merge, msg_um_keep); switch (status_ask_choice(msg_um_asktype, msg_um_choice, 3)) { case 1: load = FORCE; ret = IO_RELOAD_CTINUE; break; case 2: io_merge_data(); load = FORCE; ret = IO_RELOAD_MERGE; break; case 3: ret = IO_RELOAD_CANCEL; /* FALLTHROUGH */ default: goto cleanup; } } load = io_load_data(NULL, load); if (load == NONEW) ret = IO_RELOAD_NOOP; else if (load == NOKNOW) ret = IO_RELOAD_ERROR; cleanup: io_mutex_unlock(); mem_free(msg_um_asktype); return ret; } static void load_keys_ht_getkey(struct ht_keybindings_s *data, const char **key, int *len) { *key = data->label; *len = strlen(data->label); } static int load_keys_ht_compare(struct ht_keybindings_s *data1, struct ht_keybindings_s *data2) { const int KEYLEN = strlen(data1->label); if (strlen(data2->label) == KEYLEN && !memcmp(data1->label, data2->label, KEYLEN)) return 0; else return 1; } /* * isblank(3) is protected by the __BSD_VISIBLE macro and this fails to be * visible in some specific cases. Thus replace it by the following is_blank() * function. */ static int is_blank(int c) { return c == ' ' || c == '\t'; } /* * Load user-definable keys from file. * A hash table is used to speed up loading process in avoiding string * comparisons. * A log file is also built in case some errors were found in the key * configuration file. */ void io_load_keys(const char *pager) { struct ht_keybindings_s keys[NBKEYS]; FILE *keyfp; char buf[BUFSIZ]; struct io_file *log; int i, skipped, loaded, line; const int MAX_ERRORS = 5; keys_init(); struct ht_keybindings ht_keys = HTABLE_INITIALIZER(&ht_keys); for (i = 0; i < NBKEYS; i++) { keys[i].key = (enum key)i; keys[i].label = keys_get_label((enum key)i); HTABLE_INSERT(ht_keybindings, &ht_keys, &keys[i]); } keyfp = fopen(path_keys, "r"); EXIT_IF(keyfp == NULL, _("failed to open key file")); log = io_log_init(); skipped = loaded = line = 0; while (fgets(buf, BUFSIZ, keyfp) != NULL) { char key_label[BUFSIZ], *p; struct ht_keybindings_s *ht_elm, ht_entry; const int AWAITED = 1; int assigned; line++; if (skipped > MAX_ERRORS) { const char *too_many = _("\nToo many errors while reading configuration file!\n" "Please backup your keys file, remove it from directory, " "and launch calcurse again.\n"); io_log_print(log, line, too_many); break; } for (p = buf; is_blank((int)*p); p++) ; if (p != buf) memmove(buf, p, strlen(p)); if (buf[0] == '#' || buf[0] == '\n') continue; if (sscanf(buf, "%s", key_label) != AWAITED) { skipped++; io_log_print(log, line, _("Could not read key label")); continue; } /* Skip legacy entries. */ if (strcmp(key_label, "generic-cut") == 0) continue; ht_entry.label = key_label; p = buf + strlen(key_label) + 1; ht_elm = HTABLE_LOOKUP(ht_keybindings, &ht_keys, &ht_entry); if (!ht_elm) { skipped++; io_log_print(log, line, _("Key label not recognized")); continue; } assigned = 0; for (;;) { char key_ch[BUFSIZ], tmpbuf[BUFSIZ]; while (*p == ' ') p++; (void)strncpy(tmpbuf, p, BUFSIZ); tmpbuf[BUFSIZ - 1] = '\0'; if (sscanf(tmpbuf, "%s", key_ch) == AWAITED) { int ch; if ((ch = keys_str2int(key_ch)) < 0) { char *unknown_key; skipped++; asprintf(&unknown_key, _("Error reading key: \"%s\""), key_ch); io_log_print(log, line, unknown_key); mem_free(unknown_key); } else { int used; used = keys_assign_binding(ch, ht_elm-> key); if (used) { char *already_assigned; skipped++; asprintf(&already_assigned, _("\"%s\" assigned multiple times!"), key_ch); io_log_print(log, line, already_assigned); mem_free(already_assigned); } else { assigned++; } } p += strlen(key_ch) + 1; } else { if (assigned) loaded++; break; } } } file_close(keyfp, __FILE_POS__); file_close(log->fd, __FILE_POS__); if (skipped > 0) { const char *view_log = _("There were some errors when loading keys file."); io_log_display(log, view_log, pager); } io_log_free(log); EXIT_IF(skipped > MAX_ERRORS, _("Too many errors while reading keys file, aborting...")); if (loaded < NBKEYS) keys_fill_missing(); if (keys_check_missing_bindings()) WARN_MSG(_("Some actions do not have any associated key bindings!")); } int io_check_dir(const char *dir) { if (read_only) return -1; char *path = mem_strdup(dir); char *index; int existed = 1, failed = 0; errno = 0; for (index = path + 1; *index; index++) { if (*index == '/') { *index = '\0'; if (mkdir(path, 0700) != 0) { if (errno != EEXIST) { failed = 1; break; } } else { existed = 0; } *index = '/'; } } if (!failed && mkdir(path, 0700) != 0) { if (errno != EEXIST) failed = 1; } else { existed = 0; } if(failed) { fprintf(stderr, _("FATAL ERROR: could not create %s: %s\n"), path, strerror(errno)); exit_calcurse(EXIT_FAILURE); } mem_free(path); return existed; } unsigned io_dir_exists(const char *path) { struct stat st; return (!stat(path, &st) && S_ISDIR(st.st_mode)); } unsigned io_file_exists(const char *file) { FILE *fd; if (file && (fd = fopen(file, "r")) != NULL) { fclose(fd); return 1; } else { return 0; } } int io_check_file(const char *file) { if (read_only) return -1; errno = 0; if (io_file_exists(file)) { return 1; } else { FILE *fd; if ((fd = fopen(file, "w")) == NULL) { fprintf(stderr, _("FATAL ERROR: could not create %s: %s\n"), file, strerror(errno)); exit_calcurse(EXIT_FAILURE); } file_close(fd, __FILE_POS__); return 0; } } /* * Checks if data files exist. If not, create them. * The following structure has to be created: * * * | | * |__ apts |___ conf * |__ todo |___ keys * |__ notes/ |___ hooks/ * * Defaults: * - datadir: $XDG_DATA_HOME/calcurse (~/.local/share/calcurse) * - configdir: $XDG_CONFIG_HOME/calcurse (~/.config/calcurse) */ int io_check_data_files(void) { int missing = 0; missing += io_check_dir(path_ddir) ? 0 : 1; missing += io_check_dir(path_notes) ? 0 : 1; missing += io_check_file(path_todo) ? 0 : 1; missing += io_check_file(path_apts) ? 0 : 1; missing += io_check_dir(path_cdir) ? 0 : 1; missing += io_check_file(path_conf) ? 0 : 1; missing += io_check_dir(path_hooks) ? 0 : 1; if (!io_check_file(path_keys)) { missing++; keys_dump_defaults(path_keys); } return missing; } /* Export calcurse data. */ void io_export_data(enum export_type type, int export_uid) { FILE *stream = NULL; const char *success = _("The data were successfully exported"); const char *enter = _("Press [ENTER] to continue"); if (type < IO_EXPORT_ICAL || type >= IO_EXPORT_NBTYPES) EXIT(_("unknown export type")); switch (ui_mode) { case UI_CMDLINE: stream = stdout; break; case UI_CURSES: stream = get_export_stream(type); break; default: EXIT(_("wrong export mode")); /* NOTREACHED */ } if (stream == NULL) return; if (type == IO_EXPORT_ICAL) ical_export_data(stream, export_uid); else if (type == IO_EXPORT_PCAL) pcal_export_data(stream); if (!quiet && ui_mode == UI_CURSES) { fclose(stream); status_mesg(success, enter); keys_wait_for_any_key(win[KEY].p); } } static FILE *get_import_stream(enum import_type type, char **stream_name) { FILE *stream = NULL; const char *ask_fname = _("Enter the file name to import data from:"); const char *wrong_file = _("The file cannot be accessed, please enter another file name."); const char *press_enter = _("Press [ENTER] to continue."); *stream_name = mem_malloc(BUFSIZ); memset(*stream_name, 0, BUFSIZ); while (stream == NULL) { status_mesg(ask_fname, ""); if (updatestring(win[STA].p, stream_name, 0, 1)) { mem_free(*stream_name); return NULL; } stream = fopen(*stream_name, "r"); if (stream == NULL) { status_mesg(wrong_file, press_enter); keys_wait_for_any_key(win[KEY].p); } } return stream; } /* * Import data from a given stream (either stdin in non-interactive mode, or the * user given file in interactive mode). * A temporary log file is created in /tmp to store the import process report, * and is cleared at the end. */ void io_import_data(enum import_type type, char *stream_name, const char *fmt_ev, const char *fmt_rev, const char *fmt_apt, const char *fmt_rapt, const char *fmt_todo) { const char *proc_report = _("Import process report: %04d lines read"); char *stats_str[4]; FILE *stream = NULL; struct io_file *log; struct { unsigned events, apoints, todos, lines, skipped; } stats; EXIT_IF(type < 0 || type >= IO_IMPORT_NBTYPES, _("unknown import type")); switch (ui_mode) { case UI_CMDLINE: if (!strcmp(stream_name, "-")) stream = stdin; else stream = fopen(stream_name, "r"); EXIT_IF(stream == NULL, _("FATAL ERROR: the input file cannot be accessed, " "Aborting...")); break; case UI_CURSES: stream = get_import_stream(type, &stream_name); break; default: EXIT(_("FATAL ERROR: wrong import mode")); /* NOTREACHED */ } if (stream == NULL) return; memset(&stats, 0, sizeof stats); log = io_log_init(); if (log == NULL) { if (stream != stdin) file_close(stream, __FILE_POS__); return; } if (type == IO_IMPORT_ICAL) ical_import_data(stream_name, stream, log->fd, &stats.events, &stats.apoints, &stats.todos, &stats.lines, &stats.skipped, fmt_ev, fmt_rev, fmt_apt, fmt_rapt, fmt_todo); if (stream != stdin) file_close(stream, __FILE_POS__); if (ui_mode == UI_CURSES && (stats.apoints > 0 || stats.events > 0 || stats.todos > 0)) io_set_modified(); asprintf(&stats_str[0], ngettext("%d app", "%d apps", stats.apoints), stats.apoints); asprintf(&stats_str[1], ngettext("%d event", "%d events", stats.events), stats.events); asprintf(&stats_str[2], ngettext("%d todo", "%d todos", stats.todos), stats.todos); asprintf(&stats_str[3], _("%d skipped"), stats.skipped); if (ui_mode == UI_CURSES && !quiet) { char *read, *stat; asprintf(&read, proc_report, stats.lines); asprintf(&stat, "%s / %s / %s / %s (%s)", stats_str[0], stats_str[1], stats_str[2], stats_str[3], _("Press [ENTER] to continue")); status_mesg(read, stat); mem_free(read); mem_free(stat); keys_wait_for_any_key(win[KEY].p); } else if (ui_mode == UI_CMDLINE && !quiet) { printf(proc_report, stats.lines); printf("\n%s / %s / %s / %s\n", stats_str[0], stats_str[1], stats_str[2], stats_str[3]); } /* User has the choice to look at the log file if some items could not be imported. */ file_close(log->fd, __FILE_POS__); if (stats.skipped > 0) { const char *view_log = _("Some items could not be imported."); io_log_display(log, view_log, conf.pager); } mem_free(stats_str[0]); mem_free(stats_str[1]); mem_free(stats_str[2]); mem_free(stats_str[3]); if (ui_mode == UI_CURSES) mem_free(stream_name); if (!stats.skipped) io_log_free(log); } struct io_file *io_log_init(void) { char *logprefix, *logname; struct io_file *log = mem_malloc(sizeof(struct io_file)); if (!log) { ERROR_MSG(_("Warning: could not open temporary log file, Aborting...")); return NULL; } asprintf(&logprefix, "%s/calcurse_log", get_tempdir()); logname = new_tempfile(logprefix); if (!logname) { ERROR_MSG(_("Warning: could not create temporary log file, Aborting...")); goto error; } log->name = mem_strdup(logname); log->fd = fopen(log->name, "w"); if (log->fd == NULL) { ERROR_MSG(_("Warning: could not open temporary log file, Aborting...")); goto error; } goto cleanup; error: mem_free(log); log = NULL; cleanup: mem_free(logprefix); mem_free(logname); return log; } void io_log_print(struct io_file *log, int line, const char *msg) { if (log && log->fd) fprintf(log->fd, "line %d: %s\n", line, msg); } void io_log_display(struct io_file *log, const char *msg, const char *pager) { char *msgq; RETURN_IF(log == NULL, _("No log file to display!")); if (ui_mode == UI_CMDLINE) { fprintf(stderr, "\n%s\n", msg); fprintf(stderr, _("See %s for details."), log->name); fputc('\n', stderr); } else { asprintf(&msgq, "%s %s", msg, _("Display log file?")); if (status_ask_bool(msgq) == 1) { const char *arg[] = { pager, log->name, NULL }; wins_launch_external(arg); } mem_free(msgq); wins_erase_status_bar(); } } void io_log_free(struct io_file *log) { if (!log) return; EXIT_IF(unlink(log->name) != 0, _("Warning: could not erase temporary log file %s, Aborting..."), log->name); mem_free(log->name); mem_free(log); } /* Thread used to periodically save data. */ static void *io_psave_thread(void *arg) { int delay = conf.periodic_save; EXIT_IF(delay < 0, _("Invalid delay")); char *mesg = _("Periodic save cancelled. Data files have changed. " "Save and merge interactively"); pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL); for (;;) { sleep(delay * MININSEC); pthread_mutex_lock(&io_periodic_save_mutex); if (io_save_cal(periodic) == IO_SAVE_CANCEL) que_ins(mesg, now(), 2); pthread_mutex_unlock(&io_periodic_save_mutex); } } /* Launch the thread which handles periodic saves. */ void io_start_psave_thread(void) { pthread_create(&io_t_psave, NULL, io_psave_thread, NULL); } /* Stop periodic data saves. */ void io_stop_psave_thread(void) { /* Is the thread running? */ if (pthread_equal(io_t_psave, pthread_self())) return; /* Lock the mutex to avoid cancelling the thread during saving. */ pthread_mutex_lock(&io_periodic_save_mutex); pthread_cancel(io_t_psave); pthread_join(io_t_psave, NULL); pthread_mutex_unlock(&io_periodic_save_mutex); io_t_psave = pthread_self(); } /* * This sets a lock file to prevent from having two different instances of * calcurse running. * * If the lock cannot be obtained, then warn the user and exit calcurse. Else, * create a .calcurse.pid file in the user defined directory, which will be * removed when calcurse exits. * * Note: When creating the lock file, the interactive mode is not initialized * yet. */ void io_set_lock(void) { FILE *lock = fopen(path_cpid, "r"); int pid; if (lock != NULL) { /* If there is a lock file, check whether the process exists. */ if (fscanf(lock, "%d", &pid) == 1) { fclose(lock); if (kill(pid, 0) != 0 && errno == ESRCH) lock = NULL; } else { fclose(lock); } } if (lock != NULL) { fprintf(stderr, _("\nWARNING: it seems that another calcurse instance is " "already running.\n" "If this is not the case, please remove the following " "lock file: \n\"%s\"\n" "and restart calcurse.\n"), path_cpid); exit(EXIT_FAILURE); } if (!io_dump_pid(path_cpid)) EXIT(_("FATAL ERROR: could not create %s: %s\n"), path_cpid, strerror(errno)); } /* * Create a new file and write the process pid inside (used to create a simple * lock for example). Overwrite already existing files. */ unsigned io_dump_pid(char *file) { pid_t pid; FILE *fp; if (!file) return 0; pid = getpid(); if (!(fp = fopen(file, "w")) || fprintf(fp, "%ld\n", (long)pid) < 0 || fclose(fp) != 0) return 0; return 1; } /* * Return the pid number contained in a file previously created with * io_dump_pid (). * If no file was found, return 0. */ unsigned io_get_pid(char *file) { FILE *fp; unsigned pid; if (!file) return 0; if ((fp = fopen(file, "r")) == NULL) return 0; if (fscanf(fp, "%u", &pid) != 1) return 0; fclose(fp); return pid; } /* * Check whether a file is empty. */ int io_file_is_empty(char *file) { FILE *fp; int ret = -1; if (file && (fp = fopen(file, "r"))) { ret = (fgetc(fp) == '\n' && fgetc(fp) == EOF) || feof(fp); fclose(fp); } return ret; } /* * Check whether two files are equal. */ int io_files_equal(const char *file1, const char *file2) { FILE *fp1, *fp2; int ret = 0; if (!file1 || !file2) return 0; fp1 = fopen(file1, "rb"); fp2 = fopen(file2, "rb"); while (!feof(fp1) && !feof(fp2)) { if (fgetc(fp1) != fgetc(fp2)) goto cleanup; } ret = 1; cleanup: fclose(fp1); fclose(fp2); return ret; } /* * Copy an existing file to a new location. */ int io_file_cp(const char *src, const char *dst) { FILE *fp_src, *fp_dst; char *buffer[BUFSIZ]; unsigned int bytes_read; if (!(fp_src = fopen(src, "rb"))) return 0; if (!(fp_dst = fopen(dst, "wb"))) return 0; while (!feof(fp_src)) { bytes_read = fread(buffer, 1, BUFSIZ, fp_src); if (bytes_read > 0) { if (fwrite(buffer, 1, bytes_read, fp_dst) != bytes_read) return 0; } else { return 0; } } fclose(fp_dst); fclose(fp_src); return 1; } void io_unset_modified(void) { modified = 0; } void io_set_modified(void) { modified = 1; } int io_get_modified(void) { return modified; }