Files
calcurse-edge/src/io.c
Lars Henriksen 338c640a19 Allow undefined actions in keys configuration file
In the keys file there are three possibilities for each action:

1. One or several keys are assigned to it
2. It is marked as UNDEFINED (new)
3. It is missing from the file

On load of the keys file, calcurse respectively

1. Assigns the key(s)
2. Assigns "UNDEFINED" (new)
3. Assigns a default key if possible

If default keys were assigned, the user is informed of the number of
actions affected, and the keys file is updated.

After load each action must either have keys assigned or be undefined.
If not, calcurse exits with a failure. If there are syntax/semantic
errors in the file, calcurse rejects the file and exits.

When an interactive user leaves the keys configuration menu, a warning
is issued if any action is UNDEFINED. The keys file is always updated.

Addresses GitHub issue #298.

Additionally: Description of concepts and data structures used for
keyboard keys and virtual keys (actions) as well as name changes and
comments to improve readability.

Signed-off-by: Lars Henriksen <LarsHenriksen@get2net.dk>
Signed-off-by: Lukas Fleischer <lfleischer@calcurse.org>
2021-04-11 19:46:11 -04:00

1628 lines
38 KiB
C

/*
* Calcurse - text-based organizer
*
* Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org>
* 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 <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <signal.h>
#include <time.h>
#include <math.h>
#include <unistd.h>
#include <errno.h>
#include "calcurse.h"
#include "sha1.h"
struct ht_keybindings_s {
const char *label;
enum vkey 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 = DAY(rev->day);
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 = DAY(rapt->start);
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 = DAY(apt->start);
print_apoint(fmt_apt, day, apt);
}
LLIST_FOREACH(&eventlist, i) {
struct event *ev = LLIST_TS_GET_DATA(i);
time_t day = DAY(ev->day);
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, &lt);
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;
}
/*
* 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 virt_keys[NBVKEYS], *ht_elm, ht_entry;
FILE *keyfp;
char buf[BUFSIZ], key_label[BUFSIZ], key_str[BUFSIZ];
char *p, *msg;
struct io_file *log;
int i, n, skipped, loaded, line, assigned, undefined, key;
keys_init();
struct ht_keybindings ht_keys = HTABLE_INITIALIZER(&ht_keys);
for (i = 0; i < NBVKEYS; i++) {
virt_keys[i].key = (enum vkey)i;
virt_keys[i].label = keys_get_label((enum vkey)i);
HTABLE_INSERT(ht_keybindings, &ht_keys, &virt_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) {
line++;
p = buf;
while (*p == ' ' || *p == '\t') p++;
if (*p == '#' || *p == '\n')
continue;
/* Find the virtual key by key label. */
if (sscanf(p, "%s", key_label) != 1) {
skipped++;
io_log_print(log, line,
_("Could not read key label"));
continue;
}
p += strlen(key_label);
ht_entry.label = key_label;
ht_elm = HTABLE_LOOKUP(ht_keybindings, &ht_keys, &ht_entry);
if (!ht_elm) {
skipped++;
asprintf(&msg,
_("Key label not recognized: \"%s\""),
key_label);
io_log_print(log, line, msg);
mem_free(msg);
continue;
}
/* Assign keyboard keys to the virtual key. */
assigned = undefined = 0;
for (;;) {
if (sscanf(p, "%s%n", key_str, &n) != 1) {
if (assigned || undefined)
loaded++;
else {
skipped++;
asprintf(&msg,
_("No keys assigned to "
"\"%s\"."),
key_label);
io_log_print(log, line, msg);
mem_free(msg);
}
break;
}
p += n;
if (!strcmp(key_str, "UNDEFINED")) {
undefined++;
keys_assign_binding(-1, ht_elm->key);
} else if ((key = keys_str2int(key_str)) < 0) {
skipped++;
asprintf(&msg,
_("Keyname not recognized: \"%s\""),
key_str);
io_log_print(log, line, msg);
mem_free(msg);
} else if (keys_assign_binding(key, ht_elm->key)) {
skipped++;
asprintf(&msg,
_("\"%s\" assigned twice: \"%s\"."),
key_str, key_label);
io_log_print(log, line, msg);
mem_free(msg);
} else
assigned++;
}
}
file_close(keyfp, __FILE_POS__);
if (loaded < NBVKEYS && (i = keys_fill_missing()) < 1) {
skipped++;
strcpy(key_label, keys_get_label((enum vkey)(-i)));
strcpy(key_str, keys_get_binding((enum vkey)(-i)));
asprintf(&msg, _("Action \"%s\" absent, but default key \"%s\" "
"assigned to another action."),
key_label, key_str);
io_log_print(log, line, msg);
mem_free(msg);
}
file_close(log->fd, __FILE_POS__);
if (skipped > 0) {
msg = _("Errors in the keys file.");
io_log_display(log, msg, pager);
WARN_MSG(_("Remove offending line(s) from the keys file, "
"aborting..."));
exit_calcurse(EXIT_FAILURE);
}
io_log_free(log);
/* Default keys were inserted. */
if (loaded < NBVKEYS)
io_save_keys();
/* Should never occur. */
EXIT_IF(keys_check_missing(),
_("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:
*
* <datadir> <configdir>
* | |
* |__ 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.
*/
int 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 0;
memset(&stats, 0, sizeof stats);
log = io_log_init();
if (log == NULL) {
if (stream != stdin)
file_close(stream, __FILE_POS__);
return 0;
}
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);
return 1;
} else
return 0;
}
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 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;
}