/* * Calcurse - text-based organizer * * Copyright (c) 2004-2015 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 "calcurse.h" /* Long options */ enum { OPT_FILTER_TYPE = 1000, OPT_FILTER_HASH, OPT_FILTER_PATTERN, OPT_FILTER_START_FROM, OPT_FILTER_START_TO, OPT_FILTER_START_AFTER, OPT_FILTER_START_BEFORE, OPT_FILTER_START_RANGE, OPT_FILTER_END_FROM, OPT_FILTER_END_TO, OPT_FILTER_END_AFTER, OPT_FILTER_END_BEFORE, OPT_FILTER_END_RANGE, OPT_FILTER_PRIORITY, OPT_FILTER_COMPLETED, OPT_FILTER_UNCOMPLETED, OPT_FROM, OPT_TO, OPT_DAYS, OPT_FMT_APT, OPT_FMT_RAPT, OPT_FMT_EV, OPT_FMT_REV, OPT_FMT_TODO, OPT_LIST_IMPORTED, OPT_EXPORT_UID, OPT_READ_ONLY, OPT_STATUS, OPT_DAEMON }; /* * Print Calcurse usage and exit. */ static void usage(void) { printf("%s\n", _("usage: calcurse [--daemon|-F|-G|-g|-i|-Q|--status|-x[]]\n" " [-c] [-D] [-h] [-q] [--read-only] [-v]\n" " [--filter-*] [--format-*]")); } static void usage_try(void) { printf("%s\n", _("Try `calcurse -h` for more information.")); } /* * Print Calcurse version with a short copyright text and exit. */ static void version_arg(void) { printf(_("calcurse %s -- text-based organizer\n"), VERSION); putchar('\n'); printf("%s\n", _("Copyright (c) 2004-2015 calcurse Development Team.")); printf("%s\n", _("This is free software; see the source for copying conditions.")); } /* * Print the command line options and exit. */ static void help_arg(void) { usage(); putchar('\n'); printf("%s\n", _("Operations in non-interactive mode:")); printf("%s\n", _(" -F, --filter Read items, filter them, and write them back")); printf("%s\n", _(" -G, --grep Grep items from the data files")); printf("%s\n", _(" -Q, --query Print items in a given query range")); putchar('\n'); printf("%s\n", _("Note that -F, -G and -Q can be combined with filter options --filter-*\n" "and formatting options --format-*. Consult the man page for details.")); putchar('\n'); printf("%s\n", _("Miscellaneous:")); printf("%s\n", _(" -c, --calendar Specify the calendar data file to use")); printf("%s\n", _(" --daemon Run notification daemon in the background")); printf("%s\n", _(" -D, --directory Specify the data directory to use")); printf("%s\n", _(" -g, --gc Run the garbage collector and exit")); printf("%s\n", _(" -h, --help Show this help text and exit")); printf("%s\n", _(" -i, --import Import iCal data from a file")); printf("%s\n", _(" -q, --quiet Do not show system dialogs")); printf("%s\n", _(" --read-only Do not save configuration or data files")); printf("%s\n", _(" --status Display the status of running instances")); printf("%s\n", _(" -v, --version Show version information and exit")); printf("%s\n", _(" -x, --export [] Export items, valid formats: ical, pcal")); putchar('\n'); printf("%s\n", _("For more information, type '?' from within calcurse, or read the manpage.")); printf("%s\n", _("Submit feature requests and suggestions to .")); printf("%s\n", _("Submit bug reports to .")); } /* * Used to display the status of running instances of calcurse. * The displayed message will look like one of the following ones: * * calcurse is running (pid #) * calcurse is running in background (pid #) * calcurse is not running * * The status is obtained by looking at pid files in user data directory * (.calcurse.pid and .daemon.pid). */ static void status_arg(void) { int cpid, dpid; cpid = io_get_pid(path_cpid); dpid = io_get_pid(path_dpid); EXIT_IF(cpid && dpid, _("Error: both calcurse (pid: %d) and its daemon (pid: %d)\n" "seem to be running at the same time!\n" "Please check manually and restart calcurse.\n"), cpid, dpid); if (cpid) fprintf(stdout, _("calcurse is running (pid %d)\n"), cpid); else if (dpid) fprintf(stdout, _("calcurse is running in background (pid %d)\n"), dpid); else puts(_("calcurse is not running\n")); } /* Print TODO list and return the number of printed items. */ static int todo_arg(const char *format, int *limit, struct item_filter *filter) { const char *titlestr = filter->completed ? _("completed tasks:\n") : _("to do:\n"); int title = 1; int n = 0; llist_item_t *i; LLIST_FOREACH(&todolist, i) { if (*limit == 0) break; struct todo *todo = LLIST_TS_GET_DATA(i); if (title) { fputs(titlestr, stdout); title = 0; } print_todo(format, todo); n++; (*limit)--; } return n; } /* Print the next appointment within the upcoming 24 hours. */ static void next_arg(void) { struct notify_app next_app; const long current_time = now(); int time_left, hours_left, min_left; next_app.time = date_sec_change(current_time, 0, 1); next_app.got_app = 0; next_app.txt = NULL; next_app = *recur_apoint_check_next(&next_app, current_time, get_today()); next_app = *apoint_check_next(&next_app, current_time); if (next_app.got_app) { time_left = next_app.time - current_time; hours_left = (time_left / HOURINSEC); min_left = (time_left - hours_left * HOURINSEC) / MININSEC; fputs(_("next appointment:\n"), stdout); fprintf(stdout, " [%02d:%02d] %s\n", hours_left, min_left, next_app.txt); mem_free(next_app.txt); } } /* * Print the date on stdout. */ static void arg_print_date(long date) { char date_str[BUFSIZ]; struct tm lt; localtime_r((time_t *) & date, <); strftime(date_str, BUFSIZ, conf.output_datefmt, <); fputs(date_str, stdout); fputs(":\n", stdout); } /* * Print appointments inside the given query range. * If no start day is given (-1), today is considered. * If no end date is given (-1), a range of 1 day is considered. */ static void date_arg_from_to(long from, long to, int add_line, const char *fmt_apt, const char *fmt_rapt, const char *fmt_ev, const char *fmt_rev, int *limit) { long date; for (date = from; date < to; date = date_sec_change(date, 0, 1)) { day_store_items(date, 0); if (day_item_count(0) == 0) continue; if (add_line) fputs("\n", stdout); arg_print_date(date); day_write_stdout(date, fmt_apt, fmt_rapt, fmt_ev, fmt_rev, limit); add_line = 1; } } static time_t parse_datearg(const char *str) { struct date day; if (parse_date(str, DATEFMT_YYYYMMDD, (int *)&day.yyyy, (int *)&day.mm, (int *)&day.dd, NULL)) return date2sec(day, 0, 0); if (parse_date(str, DATEFMT_MMDDYYYY, (int *)&day.yyyy, (int *)&day.mm, (int *)&day.dd, NULL)) return date2sec(day, 0, 0); if (parse_date(str, DATEFMT_ISO, (int *)&day.yyyy, (int *)&day.mm, (int *)&day.dd, NULL)) return date2sec(day, 0, 0); return LONG_MAX; } static time_t parse_datetimearg(const char *str) { char *date = mem_strdup(str); char *time; unsigned hour, min; time_t ret; time = strchr(date, ' '); if (time) { /* Date and time. */ *time = '\0'; time++; if (!parse_time(time, &hour, &min)) return -1; ret = parse_datearg(date); if (ret == LONG_MAX) return -1; ret += hour * HOURINSEC + min * MININSEC; return ret; } ret = parse_datearg(date); if (ret == LONG_MAX) { /* No date specified, use time only. */ if (!parse_time(date, &hour, &min)) return -1; return get_today() + hour * HOURINSEC + min * MININSEC; } return ret; } static int parse_daterange(const char *str, time_t *date_from, time_t *date_to) { int ret = 0; char *s = xstrdup(str); char *p = strchr(s, ','); if (!p) goto cleanup; *p = '\0'; p++; if (*s != '\0') { *date_from = parse_datetimearg(s); if (*date_from == -1) goto cleanup; } else { *date_from = -1; } if (*p != '\0') { *date_to = parse_datetimearg(p); if (*date_to == -1) goto cleanup; } else { *date_to = -1; } ret = 1; cleanup: free(s); return ret; } static int parse_type_mask(const char *str) { char *buf = mem_strdup(str), *p; int mask = 0; for (p = strtok(buf, ","); p; p = strtok(NULL, ",")) { if (!strcmp(p, "event")) { mask |= TYPE_MASK_EVNT; } else if (!strcmp(p, "apt")) { mask |= TYPE_MASK_APPT; } else if (!strcmp(p, "recur-event")) { mask |= TYPE_MASK_RECUR_EVNT; } else if (!strcmp(p, "recur-apt")) { mask |= TYPE_MASK_RECUR_APPT; } else if (!strcmp(p, "recur")) { mask |= TYPE_MASK_RECUR; } else if (!strcmp(p, "cal")) { mask |= TYPE_MASK_CAL; } else if (!strcmp(p, "todo")) { mask |= TYPE_MASK_TODO; } else { mask = 0; goto cleanup; } } cleanup: mem_free(buf); return mask; } /* * Parse the command-line arguments and call the appropriate * routines to handle those arguments. Also initialize the data paths. */ int parse_args(int argc, char **argv) { /* Command-line flags */ int grep = 0, grep_filter = 0, query = 0, next = 0; int status = 0, gc = 0, import = 0, export = 0, daemon = 0; /* Query ranges */ time_t from = -1, to = -1; int range = -1; int limit = INT_MAX; /* Filters */ struct item_filter filter = { 0, NULL, NULL, -1, -1, -1, -1, 0, 0, 0 }; /* Format strings */ const char *fmt_apt = " - %S -> %E\n\t%m\n"; const char *fmt_rapt = " - %S -> %E\n\t%m\n"; const char *fmt_ev = " * %m\n"; const char *fmt_rev = " * %m\n"; const char *fmt_todo = "%p. %m\n"; /* Import and export parameters */ int xfmt = IO_EXPORT_ICAL; int list_imported = 0, export_uid = 0; /* Data file locations */ const char *cfile = NULL, *datadir = NULL, *ifile = NULL; int non_interactive = 1; int ch; regex_t reg; static const char *optstr = "FgGhvnNax::t::d:c:r::s::S:D:i:l:qQ"; struct option longopts[] = { {"appointment", no_argument, NULL, 'a'}, {"calendar", required_argument, NULL, 'c'}, {"day", required_argument, NULL, 'd'}, {"directory", required_argument, NULL, 'D'}, {"filter", no_argument, NULL, 'F'}, {"gc", no_argument, NULL, 'g'}, {"grep", no_argument, NULL, 'G'}, {"help", no_argument, NULL, 'h'}, {"import", required_argument, NULL, 'i'}, {"limit", required_argument, NULL, 'l'}, {"next", no_argument, NULL, 'n'}, {"note", no_argument, NULL, 'N'}, {"range", optional_argument, NULL, 'r'}, {"startday", optional_argument, NULL, 's'}, {"search", required_argument, NULL, 'S'}, {"todo", optional_argument, NULL, 't'}, {"version", no_argument, NULL, 'v'}, {"export", optional_argument, NULL, 'x'}, {"quiet", no_argument, NULL, 'q'}, {"query", optional_argument, NULL, 'Q'}, {"filter-type", required_argument, NULL, OPT_FILTER_TYPE}, {"filter-hash", required_argument, NULL, OPT_FILTER_HASH}, {"filter-pattern", required_argument, NULL, OPT_FILTER_PATTERN}, {"filter-start-from", required_argument, NULL, OPT_FILTER_START_FROM}, {"filter-start-to", required_argument, NULL, OPT_FILTER_START_TO}, {"filter-start-after", required_argument, NULL, OPT_FILTER_START_AFTER}, {"filter-start-before", required_argument, NULL, OPT_FILTER_START_BEFORE}, {"filter-start-range", required_argument, NULL, OPT_FILTER_START_RANGE}, {"filter-end-from", required_argument, NULL, OPT_FILTER_END_FROM}, {"filter-end-to", required_argument, NULL, OPT_FILTER_END_TO}, {"filter-end-after", required_argument, NULL, OPT_FILTER_END_AFTER}, {"filter-end-before", required_argument, NULL, OPT_FILTER_END_BEFORE}, {"filter-end-range", required_argument, NULL, OPT_FILTER_END_RANGE}, {"filter-priority", required_argument, NULL, OPT_FILTER_PRIORITY}, {"filter-completed", no_argument, NULL, OPT_FILTER_COMPLETED}, {"filter-uncompleted", no_argument, NULL, OPT_FILTER_UNCOMPLETED}, {"from", required_argument, NULL, OPT_FROM}, {"to", required_argument, NULL, OPT_TO}, {"days", required_argument, NULL, OPT_DAYS}, {"format-apt", required_argument, NULL, OPT_FMT_APT}, {"format-recur-apt", required_argument, NULL, OPT_FMT_RAPT}, {"format-event", required_argument, NULL, OPT_FMT_EV}, {"format-recur-event", required_argument, NULL, OPT_FMT_REV}, {"format-todo", required_argument, NULL, OPT_FMT_TODO}, {"export-uid", no_argument, NULL, OPT_EXPORT_UID}, {"list-imported", no_argument, NULL, OPT_LIST_IMPORTED}, {"read-only", no_argument, NULL, OPT_READ_ONLY}, {"status", no_argument, NULL, OPT_STATUS}, {"daemon", no_argument, NULL, OPT_DAEMON}, {NULL, no_argument, NULL, 0} }; while ((ch = getopt_long(argc, argv, optstr, longopts, NULL)) != -1) { switch (ch) { case 'a': filter.type_mask |= TYPE_MASK_CAL; query = 1; break; case 'c': cfile = optarg; break; case 'd': if (is_all_digit(optarg)) { range = atoi(optarg); } else { from = parse_datetimearg(optarg); EXIT_IF(from == -1, _("invalid date: %s"), optarg); } filter.type_mask |= TYPE_MASK_CAL; query = 1; break; case 'D': datadir = optarg; break; case 'F': grep_filter = grep = 1; break; case 'h': help_arg(); goto cleanup; case 'g': gc = 1; break; case 'G': grep = 1; break; case 'i': import = 1; ifile = optarg; break; case 'l': limit = atoi(optarg); break; case 'n': next = 1; break; case 'r': if (optarg) range = atoi(optarg); else range = 1; EXIT_IF(range == 0, _("invalid range: %s"), optarg); filter.type_mask |= TYPE_MASK_CAL; query = 1; break; case 's': from = parse_datetimearg(optarg); EXIT_IF(from == -1, _("invalid date: %s"), optarg); filter.type_mask |= TYPE_MASK_CAL; query = 1; break; case 't': if (optarg) { EXIT_IF(!is_all_digit(optarg), _("invalid priority: %s"), optarg); filter.priority = atoi(optarg); if (filter.priority == 0) filter.completed = 1; else filter.uncompleted = 1; EXIT_IF(filter.priority > 9, _("invalid priority: %s"), optarg); } else { filter.uncompleted = 1; } filter.type_mask |= TYPE_MASK_TODO; query = 1; break; case 'v': version_arg(); goto cleanup; case 'x': export = 1; if (optarg) { if (!strcmp(optarg, "ical")) xfmt = IO_EXPORT_ICAL; else if (!strcmp(optarg, "pcal")) xfmt = IO_EXPORT_PCAL; else EXIT(_("invalid export format: %s"), optarg); } break; case 'q': quiet = 1; break; case 'Q': query = 1; break; case OPT_FILTER_TYPE: filter.type_mask = parse_type_mask(optarg); EXIT_IF(filter.type_mask == 0, _("invalid filter mask")); break; case OPT_FILTER_HASH: filter.hash = mem_strdup(optarg); break; case 'S': case OPT_FILTER_PATTERN: EXIT_IF(filter.regex, _("cannot handle more than one regular expression")); if (regcomp(®, optarg, REG_EXTENDED)) EXIT(_("could not compile regular expression: %s"), optarg); filter.regex = ® break; case OPT_FILTER_START_FROM: filter.start_from = parse_datetimearg(optarg); EXIT_IF(filter.start_from == -1, _("invalid date: %s"), optarg); break; case OPT_FILTER_START_TO: filter.start_to = parse_datetimearg(optarg); EXIT_IF(filter.start_to == -1, _("invalid date: %s"), optarg); break; case OPT_FILTER_START_AFTER: filter.start_from = parse_datetimearg(optarg) + 1; EXIT_IF(filter.start_from == -1, _("invalid date: %s"), optarg); break; case OPT_FILTER_START_BEFORE: filter.start_to = parse_datetimearg(optarg) - 1; EXIT_IF(filter.start_to == -1, _("invalid date: %s"), optarg); break; case OPT_FILTER_START_RANGE: EXIT_IF(!parse_daterange(optarg, &filter.start_from, &filter.start_to), _("invalid date range: %s"), optarg); break; case OPT_FILTER_END_FROM: filter.end_from = parse_datetimearg(optarg); EXIT_IF(filter.end_from == -1, _("invalid date: %s"), optarg); break; case OPT_FILTER_END_TO: filter.end_to = parse_datetimearg(optarg); EXIT_IF(filter.end_to == -1, _("invalid date: %s"), optarg); break; case OPT_FILTER_END_AFTER: filter.end_from = parse_datetimearg(optarg) + 1; EXIT_IF(filter.end_from == -1, _("invalid date: %s"), optarg); break; case OPT_FILTER_END_BEFORE: filter.end_to = parse_datetimearg(optarg) - 1; EXIT_IF(filter.end_to == -1, _("invalid date: %s"), optarg); break; case OPT_FILTER_END_RANGE: EXIT_IF(!parse_daterange(optarg, &filter.end_from, &filter.end_to), _("invalid date range: %s"), optarg); break; case OPT_FILTER_PRIORITY: filter.priority = atoi(optarg); EXIT_IF(filter.priority < 1 || filter.priority > 9, _("invalid priority: %s"), optarg); break; case OPT_FILTER_COMPLETED: filter.completed = 1; break; case OPT_FILTER_UNCOMPLETED: filter.uncompleted = 1; break; case OPT_FROM: from = parse_datetimearg(optarg); EXIT_IF(from == -1, _("invalid date: %s"), optarg); break; case OPT_TO: to = parse_datetimearg(optarg); EXIT_IF(to == -1, _("invalid date: %s"), optarg); break; case OPT_DAYS: range = atoi(optarg); EXIT_IF(range == 0, _("invalid range: %s"), optarg); break; case OPT_FMT_APT: fmt_apt = optarg; break; case OPT_FMT_RAPT: fmt_rapt = optarg; break; case OPT_FMT_EV: fmt_ev = optarg; break; case OPT_FMT_REV: fmt_rev = optarg; break; case OPT_FMT_TODO: fmt_todo = optarg; break; case OPT_LIST_IMPORTED: list_imported = 1; break; case OPT_EXPORT_UID: export_uid = 1; break; case OPT_READ_ONLY: read_only = 1; break; case OPT_STATUS: status = 1; break; case OPT_DAEMON: daemon = 1; break; default: usage(); usage_try(); goto cleanup; } } argc -= optind; if (filter.type_mask == 0) filter.type_mask = TYPE_MASK_ALL; if (status + grep + query + next + gc + import + export + daemon > 1) { ERROR_MSG(_("invalid argument combination")); usage(); usage_try(); goto cleanup; } if (from == -1) { struct date day = { 0, 0, 0 }; from = get_sec_date(day); } if (to == -1 && range == -1) to = date_sec_change(from, 0, 1); else if (to == -1 && range >= 0) to = date_sec_change(from, 0, range); else if (to >= 0 && range >= 0) EXIT_IF(to >= 0, _("cannot specify a range and an end date")); io_init(cfile, datadir); io_check_dir(path_dir); io_check_dir(path_notes); if (status) { status_arg(); } else if (grep) { io_check_file(path_apts); io_check_file(path_todo); io_check_file(path_conf); vars_init(); config_load(); /* To get output date format. */ io_load_data(&filter); io_save_todo(grep_filter ? path_todo : NULL); io_save_apts(grep_filter ? path_apts : NULL); } else if (query) { io_check_file(path_apts); io_check_file(path_todo); io_check_file(path_conf); vars_init(); config_load(); /* To get output date format. */ io_load_data(&filter); int add_line = todo_arg(fmt_todo, &limit, &filter); date_arg_from_to(from, to, add_line, fmt_apt, fmt_rapt, fmt_ev, fmt_rev, &limit); } else if (next) { io_check_file(path_apts); io_load_app(&filter); next_arg(); } else if (gc) { io_check_file(path_apts); io_check_file(path_todo); io_load_data(NULL); note_gc(); } else if (import) { io_check_file(path_apts); io_check_file(path_todo); /* Get default pager in case we need to show a log file. */ vars_init(); io_load_data(NULL); io_import_data(IO_IMPORT_ICAL, ifile, list_imported); io_save_apts(path_apts); io_save_todo(path_todo); } else if (export) { io_check_file(path_apts); io_check_file(path_todo); io_load_data(&filter); io_export_data(xfmt, export_uid); } else if (daemon) { notify_init_vars(); dmon_start(0); } else { /* interactive mode */ non_interactive = 0; } cleanup: /* Free filter parameters. */ if (filter.regex) regfree(filter.regex); return non_interactive; }