diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/apoint.c | 31 | ||||
-rw-r--r-- | src/args.c | 39 | ||||
-rw-r--r-- | src/calcurse.c | 201 | ||||
-rw-r--r-- | src/calcurse.h | 176 | ||||
-rw-r--r-- | src/config.c | 32 | ||||
-rw-r--r-- | src/custom.c | 93 | ||||
-rw-r--r-- | src/day.c | 106 | ||||
-rw-r--r-- | src/dmon.c | 10 | ||||
-rw-r--r-- | src/event.c | 20 | ||||
-rw-r--r-- | src/getstring.c | 4 | ||||
-rw-r--r-- | src/help.c | 6 | ||||
-rw-r--r-- | src/hooks.c | 37 | ||||
-rw-r--r-- | src/htable.h | 2 | ||||
-rw-r--r-- | src/ical.c | 1494 | ||||
-rw-r--r-- | src/io.c | 378 | ||||
-rw-r--r-- | src/keys.c | 335 | ||||
-rw-r--r-- | src/listbox.c | 2 | ||||
-rw-r--r-- | src/llist.c | 204 | ||||
-rw-r--r-- | src/llist.h | 5 | ||||
-rw-r--r-- | src/llist_ts.h | 4 | ||||
-rw-r--r-- | src/mem.c | 2 | ||||
-rw-r--r-- | src/note.c | 24 | ||||
-rw-r--r-- | src/notify.c | 38 | ||||
-rw-r--r-- | src/pcal.c | 16 | ||||
-rw-r--r-- | src/queue.c | 2 | ||||
-rw-r--r-- | src/recur.c | 1171 | ||||
-rw-r--r-- | src/sha1.c | 3 | ||||
-rw-r--r-- | src/sha1.h | 4 | ||||
-rw-r--r-- | src/sigs.c | 20 | ||||
-rw-r--r-- | src/strings.c | 2 | ||||
-rw-r--r-- | src/todo.c | 6 | ||||
-rw-r--r-- | src/ui-calendar.c | 123 | ||||
-rw-r--r-- | src/ui-day.c | 932 | ||||
-rw-r--r-- | src/ui-todo.c | 49 | ||||
-rw-r--r-- | src/utf8.c | 2 | ||||
-rw-r--r-- | src/utils.c | 213 | ||||
-rw-r--r-- | src/vars.c | 8 | ||||
-rw-r--r-- | src/vector.c | 2 | ||||
-rw-r--r-- | src/vector.h | 2 | ||||
-rw-r--r-- | src/wins.c | 26 |
40 files changed, 4078 insertions, 1746 deletions
diff --git a/src/apoint.c b/src/apoint.c index 77ff8a0..e138e5e 100644 --- a/src/apoint.c +++ b/src/apoint.c @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -42,6 +42,8 @@ #include "calcurse.h" #include "sha1.h" +#define APPT_TIME_LENGTH 25 + llist_ts_t alist_p; void apoint_free(struct apoint *apt) @@ -134,16 +136,14 @@ void apoint_sec2str(struct apoint *o, time_t day, char *start, char *end) } else { t = o->start; localtime_r(&t, <); - snprintf(start, HRMIN_SIZE, "%02u:%02u", lt.tm_hour, - lt.tm_min); + strftime(start, APPT_TIME_LENGTH, conf.timefmt, <); } if (o->start + o->dur > day + DAYLEN(day)) { strncpy(end, "..:..", 6); } else { t = o->start + o->dur; localtime_r(&t, <); - snprintf(end, HRMIN_SIZE, "%02u:%02u", lt.tm_hour, - lt.tm_min); + strftime(end, APPT_TIME_LENGTH, conf.timefmt, <); } } @@ -195,7 +195,7 @@ void apoint_write(struct apoint *o, FILE * f) mem_free(str); } -struct apoint *apoint_scan(FILE * f, struct tm start, struct tm end, +char *apoint_scan(FILE * f, struct tm start, struct tm end, char state, char *note, struct item_filter *filter) { char buf[BUFSIZ], *newline; @@ -203,15 +203,15 @@ struct apoint *apoint_scan(FILE * f, struct tm start, struct tm end, struct apoint *apt = NULL; int cond; - EXIT_IF(!check_date(start.tm_year, start.tm_mon, start.tm_mday) || - !check_date(end.tm_year, end.tm_mon, end.tm_mday) || - !check_time(start.tm_hour, start.tm_min) || - !check_time(end.tm_hour, end.tm_min), - _("date error in appointment")); + if (!check_date(start.tm_year, start.tm_mon, start.tm_mday) || + !check_date(end.tm_year, end.tm_mon, end.tm_mday) || + !check_time(start.tm_hour, start.tm_min) || + !check_time(end.tm_hour, end.tm_min)) + return _("illegal date in appointment"); /* Read the appointment description */ if (!fgets(buf, sizeof buf, f)) - return NULL; + return _("error in appointment description"); newline = strchr(buf, '\n'); if (newline) @@ -226,8 +226,8 @@ struct apoint *apoint_scan(FILE * f, struct tm start, struct tm end, tstart = mktime(&start); tend = mktime(&end); - EXIT_IF(tstart == -1 || tend == -1 || tstart > tend, - _("date error in appointment")); + if (tstart == -1 || tend == -1 || tstart > tend) + return _("date error in appointment"); /* Filter item. */ if (filter) { @@ -255,8 +255,7 @@ struct apoint *apoint_scan(FILE * f, struct tm start, struct tm end, } if (!apt) apt = apoint_new(buf, note, tstart, tend - tstart, state); - - return apt; + return NULL; } void apoint_delete(struct apoint *apt) @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -97,7 +97,7 @@ static void usage(void) "calcurse [-D <directory>] [-C <directory>] [-c <calendar file>]\n" "calcurse -Q [--from <date>] [--to <date>] [--days <number>]\n" "calcurse -a | -d <date> | -d <number> | -n | -r[<number>] | -s[<date>] | -t[<number>]\n" - "calcurse -h | -v | --status | -G | -P | -g | -i <file> | -x[<file>] | --daemon")); + "calcurse -h | -v | --status | -G | -P | -g | -i <file> | -x[<format>] | --daemon")); } static void usage_try(void) @@ -112,7 +112,7 @@ static void version_arg(void) { printf(_("calcurse %s -- text-based organizer\n"), VERSION); putchar('\n'); - printf("%s\n", _("Copyright (c) 2004-2020 calcurse Development Team.")); + printf("%s\n", _("Copyright (c) 2004-2023 calcurse Development Team.")); printf("%s\n", _("This is free software; see the source for copying conditions.")); } @@ -149,7 +149,7 @@ static void help_arg(void) printf("%s\n", _(" -g, --gc Run the garbage collector")); printf("%s\n", _(" -h, --help Show this help text")); printf("%s\n", _(" -i, --import <file> Import iCal data from file")); - printf("%s\n", _(" -q, --quiet Suppress system dialogs")); + printf("%s\n", _(" -q, --quiet Suppress import/export result message")); printf("%s\n", _(" --read-only Do not save configuration or data files")); printf("%s\n", _(" --status Display status of running instances")); printf("%s\n", _(" -v, --version Show version information")); @@ -398,7 +398,7 @@ cleanup: /* * Parse the command-line arguments and call the appropriate * routines to handle those arguments. Also initialize the data paths. - * Returns the non-interactive value. + * Exit here in case of errors else return the non-interactive value. */ int parse_args(int argc, char **argv) { @@ -428,7 +428,7 @@ int parse_args(int argc, char **argv) const char *cfile = NULL, *confdir = NULL; char *ifile = NULL; - int non_interactive = 1; + int ret, non_interactive = 1; int ch, cpid, type; regex_t reg; char buf[BUFSIZ]; @@ -515,6 +515,10 @@ int parse_args(int argc, char **argv) case 'c': cfile = optarg; break; + case '?': + usage(); + usage_try(); + exit(EXIT_FAILURE); \ } } io_init(cfile, datadir, confdir); @@ -557,7 +561,7 @@ int parse_args(int argc, char **argv) break; case 'h': help_arg(); - goto cleanup; + exit(EXIT_SUCCESS); case 'g': gc = 1; break; @@ -615,7 +619,7 @@ int parse_args(int argc, char **argv) break; case 'v': version_arg(); - goto cleanup; + exit(EXIT_SUCCESS); case 'x': export = 1; if (optarg) { @@ -860,10 +864,6 @@ int parse_args(int argc, char **argv) '\0'; cmd_line = 1; break; - default: - usage(); - usage_try(); - goto cleanup; } } @@ -876,12 +876,8 @@ int parse_args(int argc, char **argv) (format_opt && !(grep + query + dump_imported)) || (query_range && !query) || (purge && !filter.invert) - ) { - ERROR_MSG(_("invalid argument combination")); - usage(); - usage_try(); - goto cleanup; - } + ) + EXIT(_("invalid argument combination")); EXIT_IF(to >= 0 && range, _("cannot specify a range and an end date")); if (from == -1) @@ -967,10 +963,12 @@ int parse_args(int argc, char **argv) fmt_apt = fmt_rapt = fmt_ev = fmt_rev = NULL; fmt_todo = NULL; } - io_import_data(IO_IMPORT_ICAL, ifile, fmt_ev, fmt_rev, fmt_apt, - fmt_rapt, fmt_todo); + ret = io_import_data(IO_IMPORT_ICAL, ifile, fmt_ev, fmt_rev, + fmt_apt, fmt_rapt, fmt_todo); io_save_apts(path_apts); io_save_todo(path_todo); + if (!ret) + exit_calcurse(EXIT_FAILURE); } else if (export) { io_check_file(path_apts); io_check_file(path_todo); @@ -984,7 +982,6 @@ int parse_args(int argc, char **argv) non_interactive = 0; } -cleanup: /* Free filter parameters. */ if (filter.regex) regfree(filter.regex); diff --git a/src/calcurse.c b/src/calcurse.c index 1f0696d..c89d1dd 100644 --- a/src/calcurse.c +++ b/src/calcurse.c @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -41,36 +41,6 @@ #define HANDLE_KEY(key, fn) case key: fn(); break; int count, reg; -/* - * Store events and appointments for a range of days in the day vector - - * beginning with the selected day - and load them into the APP listbox. If no - * day-change occurs, reset the selected APP item and with it the selected day, - * thereby storing and loading the same range of days. - */ -static void do_storage(int day_changed) -{ - /* - * Save the selected item before rebuilding the day vector - - * unless already set. - */ - if (!day_check_sel_data()) - day_set_sel_data(ui_day_get_sel()); - - if (!day_changed) - ui_day_sel_reset(); - - /* The day_items vector. */ - day_store_items(get_slctd_day(), 1, day_get_days()); - /* The APP listbox. */ - ui_day_load_items(); - - if (day_changed) - ui_day_sel_reset(); - else - ui_day_find_sel(); - - day_set_sel_data(&empty_day); -} static inline void key_generic_change_view(void) { @@ -79,6 +49,13 @@ static inline void key_generic_change_view(void) wins_update(FLAG_ALL); } +static inline void key_generic_prev_view(void) +{ + wins_reset_status_page(); + wins_slctd_prev(); + wins_update(FLAG_ALL); +} + static inline void key_generic_other_cmd(void) { wins_other_status_page(); @@ -90,7 +67,7 @@ static inline void key_generic_goto(void) wins_erase_status_bar(); ui_calendar_set_current_date(); ui_calendar_change_day(conf.input_datefmt); - do_storage(1); + day_do_storage(1); wins_update(FLAG_CAL | FLAG_APP | FLAG_STA); } @@ -99,7 +76,7 @@ static inline void key_generic_goto_today(void) wins_erase_status_bar(); ui_calendar_set_current_date(); ui_calendar_goto_today(); - do_storage(1); + day_do_storage(1); wins_update(FLAG_CAL | FLAG_APP | FLAG_STA); } @@ -117,14 +94,14 @@ static inline void key_generic_config_menu(void) wins_erase_status_bar(); wins_reset_status_page(); custom_config_main(); - do_storage(0); + day_do_storage(0); wins_update(FLAG_ALL); } static inline void key_generic_add_appt(void) { ui_day_item_add(); - do_storage(0); + day_do_storage(0); wins_update(FLAG_CAL | FLAG_APP | FLAG_STA); } @@ -140,7 +117,7 @@ static inline void key_add_item(void) case APP: case CAL: ui_day_item_add(); - do_storage(0); + day_do_storage(0); wins_update(FLAG_CAL | FLAG_APP | FLAG_STA); break; case TOD: @@ -156,7 +133,7 @@ static inline void key_edit_item(void) { if (wins_slctd() == APP && !event_dummy(ui_day_get_sel())) { ui_day_item_edit(); - do_storage(0); + day_do_storage(0); wins_update(FLAG_CAL | FLAG_APP | FLAG_STA); } else if (wins_slctd() == TOD) { ui_todo_edit(); @@ -168,7 +145,7 @@ static inline void key_del_item(void) { if (wins_slctd() == APP && !event_dummy(ui_day_get_sel())) { ui_day_item_delete(reg); - do_storage(0); + day_do_storage(0); wins_update(FLAG_CAL | FLAG_APP | FLAG_STA); } else if (wins_slctd() == TOD) { ui_todo_delete(); @@ -186,7 +163,7 @@ static inline void key_generic_paste(void) { if (wins_slctd() == APP) { ui_day_item_paste(reg); - do_storage(0); + day_do_storage(0); wins_update(FLAG_CAL | FLAG_APP); } } @@ -195,7 +172,7 @@ static inline void key_repeat_item(void) { if (wins_slctd() == APP && !event_dummy(ui_day_get_sel())) { ui_day_item_repeat(); - do_storage(0); + day_do_storage(0); wins_update(FLAG_CAL | FLAG_APP | FLAG_STA); } } @@ -204,7 +181,7 @@ static inline void key_flag_item(void) { if (wins_slctd() == APP && !event_dummy(ui_day_get_sel())) { ui_day_flag(); - do_storage(0); + day_do_storage(0); wins_update(FLAG_APP); } else if (wins_slctd() == TOD) { ui_todo_flag(); @@ -243,7 +220,7 @@ static inline void key_edit_note(void) { if (wins_slctd() == APP && !event_dummy(ui_day_get_sel())) { ui_day_edit_note(); - do_storage(0); + day_do_storage(0); } else if (wins_slctd() == TOD) { ui_todo_edit_note(); } @@ -283,7 +260,7 @@ static inline void key_generic_save(void) if (ret == IO_SAVE_RELOAD) { ui_todo_load_items(); ui_todo_sel_reset(); - do_storage(0); + day_do_storage(0); notify_check_next_app(1); ui_calendar_monthly_view_cache_set_invalid(); } @@ -318,7 +295,7 @@ static inline void key_generic_reload(void) ret == IO_RELOAD_MERGE) { ui_todo_load_items(); ui_todo_sel_reset(); - do_storage(0); + day_do_storage(0); notify_check_next_app(1); ui_calendar_monthly_view_cache_set_invalid(); } @@ -348,7 +325,7 @@ static inline void key_generic_import(void) wins_erase_status_bar(); io_import_data(IO_IMPORT_ICAL, NULL, NULL, NULL, NULL, NULL, NULL); ui_calendar_monthly_view_cache_set_invalid(); - do_storage(0); + day_do_storage(0); ui_todo_load_items(); wins_update(FLAG_ALL); } @@ -373,14 +350,14 @@ static inline void key_generic_export() break; } - do_storage(0); + day_do_storage(0); wins_update(FLAG_ALL); } static inline void key_generic_prev_day(void) { ui_calendar_move(DAY_PREV, count); - do_storage(1); + day_do_storage(1); wins_update(FLAG_CAL | FLAG_APP); } @@ -393,7 +370,7 @@ static inline void key_move_left(void) static inline void key_generic_next_day(void) { ui_calendar_move(DAY_NEXT, count); - do_storage(1); + day_do_storage(1); wins_update(FLAG_CAL | FLAG_APP); } @@ -406,7 +383,7 @@ static inline void key_move_right(void) static inline void key_generic_prev_week(void) { ui_calendar_move(WEEK_PREV, count); - do_storage(1); + day_do_storage(1); wins_update(FLAG_CAL | FLAG_APP); } @@ -417,7 +394,7 @@ static inline void key_move_up(void) } else if (wins_slctd() == APP) { if (!ui_day_sel_move(-1)) { ui_calendar_move(DAY_PREV, 1); - do_storage(1); + day_do_storage(1); ui_day_sel_dayend(); } wins_update(FLAG_APP | FLAG_CAL); @@ -430,7 +407,7 @@ static inline void key_move_up(void) static inline void key_generic_next_week(void) { ui_calendar_move(WEEK_NEXT, count); - do_storage(1); + day_do_storage(1); wins_update(FLAG_CAL | FLAG_APP); } @@ -441,7 +418,7 @@ static inline void key_move_down(void) } else if (wins_slctd() == APP) { if (!ui_day_sel_move(1)) { ui_calendar_move(DAY_PREV, day_get_days() - 2); - do_storage(1); + day_do_storage(1); ui_day_sel_daybegin(day_get_days() - 1); } wins_update(FLAG_APP | FLAG_CAL); @@ -454,28 +431,28 @@ static inline void key_move_down(void) static inline void key_generic_prev_month(void) { ui_calendar_move(MONTH_PREV, count); - do_storage(1); + day_do_storage(1); wins_update(FLAG_CAL | FLAG_APP); } static inline void key_generic_next_month(void) { ui_calendar_move(MONTH_NEXT, count); - do_storage(1); + day_do_storage(1); wins_update(FLAG_CAL | FLAG_APP); } static inline void key_generic_prev_year(void) { ui_calendar_move(YEAR_PREV, count); - do_storage(1); + day_do_storage(1); wins_update(FLAG_CAL | FLAG_APP); } static inline void key_generic_next_year(void) { ui_calendar_move(YEAR_NEXT, count); - do_storage(1); + day_do_storage(1); wins_update(FLAG_CAL | FLAG_APP); } @@ -483,7 +460,7 @@ static inline void key_start_of_week(void) { if (wins_slctd() == CAL) { ui_calendar_move(WEEK_START, count); - do_storage(1); + day_do_storage(1); wins_update(FLAG_CAL | FLAG_APP); } } @@ -492,7 +469,7 @@ static inline void key_end_of_week(void) { if (wins_slctd() == CAL) { ui_calendar_move(WEEK_END, count); - do_storage(1); + day_do_storage(1); wins_update(FLAG_CAL | FLAG_APP); } } @@ -556,7 +533,8 @@ static inline void key_generic_cmd(void) int valid = 0, force = 0, ret; char *error_msg; - status_mesg(_("Command: [ h(elp) | w(rite)(!) | q(uit)(!) | wq(!) ]"), ""); + status_mesg(_("Command: " + "[ h(elp) | w(rite)(!) | q(uit)(!) | wq(!) | n(ext) | p(rev) ]"), ""); if (getstring(win[STA].p, cmd, BUFSIZ, 0, 1) != GETSTRING_VALID) goto cleanup; cmd_name = strtok(cmd, " "); @@ -608,6 +586,96 @@ static inline void key_generic_cmd(void) valid = 1; } + if (!strcmp(cmd_name, "next") || !strcmp(cmd_name, "n")) { + struct day_item *item; + time_t day, next; + struct recur_apoint *rapt; + struct recur_event *rev; + int more = 0; + + if (wins_slctd() != APP) { + error_msg = + _("Select a repeating item in the appointments panel."); + warnbox(error_msg); + goto cleanup; + } + item = ui_day_get_sel(); + /* + * The selected day need not be the (item) start day + * for multi-day occurrences. + */ + day = DAY(item->start); + if (item->type == RECUR_EVNT) { + rev = item->item.rev; + more = recur_next_occurrence(rev->day, -1, rev->rpt, &rev->exc, + day, &next); + } else if (item->type == RECUR_APPT) { + rapt = item->item.rapt; + more = recur_next_occurrence(rapt->start, rapt->dur, rapt->rpt, + &rapt->exc, day, &next); + } else { + error_msg = _("Not a repeating item."); + warnbox(error_msg); + goto cleanup; + } + if (!more) { + error_msg = _("Last occurrence."); + warnbox(error_msg); + goto cleanup; + } + item->order = next; + ui_calendar_set_slctd_day(sec2date(next)); + day_set_sel_data(item); + day_do_storage(1); + + valid = 1; + } + if (!strcmp(cmd_name, "prev") || !strcmp(cmd_name, "p")) { + struct day_item *item; + time_t day, prev; + struct recur_apoint *rapt; + struct recur_event *rev; + int more = 0; + + if (wins_slctd() != APP) { + error_msg = _("Select a repeating item in the" + " appointments panel."); + warnbox(error_msg); + goto cleanup; + } + item = ui_day_get_sel(); + /* + * The selected day need not be the (item) start day + * for multi-day occurrences. + */ + day = DAY(item->start); + if (item->type == RECUR_EVNT) { + rev = item->item.rev; + more = recur_prev_occurrence(rev->day, -1, rev->rpt, + &rev->exc, day, &prev); + } else if (item->type == RECUR_APPT) { + rapt = item->item.rapt; + more = recur_prev_occurrence(rapt->start, rapt->dur, + rapt->rpt, &rapt->exc, + day, &prev); + } else { + error_msg = _("Not a repeating item."); + warnbox(error_msg); + goto cleanup; + } + if (!more) { + error_msg = _("First occurrence."); + warnbox(error_msg); + goto cleanup; + } + item->order = prev; + ui_calendar_set_slctd_day(sec2date(prev)); + day_set_sel_data(item); + day_do_storage(1); + + valid = 1; + } + if (!valid) { asprintf(&error_msg, _("No such command: %s"), cmd); status_mesg(error_msg, ""); @@ -628,8 +696,6 @@ cleanup: */ int main(int argc, char **argv) { - int no_data_file = 1; - #if ENABLE_NLS setlocale(LC_ALL, ""); bindtextdomain(PACKAGE, LOCALEDIR); @@ -644,7 +710,7 @@ int main(int argc, char **argv) /* Non-interactive mode. */ exit_calcurse(EXIT_SUCCESS); } else { - no_data_file = io_check_data_files(); + io_check_data_files(); dmon_stop(); io_set_lock(); } @@ -714,12 +780,8 @@ int main(int argc, char **argv) * implicitly calling wrefresh() later (causing ncurses race conditions). */ wins_wrefresh(win[KEY].p); - if (show_dialogs()) { - wins_update(FLAG_ALL); - io_startup_screen(no_data_file); - } ui_calendar_monthly_view_cache_set_invalid(); - do_storage(1); + day_do_storage(1); ui_todo_load_items(); ui_todo_sel_reset(); wins_update(FLAG_ALL); @@ -739,7 +801,7 @@ int main(int argc, char **argv) que_show(); if (conf.systemevents) { que_save(); - do_storage(0); + day_do_storage(0); } wins_update(FLAG_ALL); que_rem(); @@ -749,7 +811,7 @@ int main(int argc, char **argv) resize = 0; wins_reset(); if (conf.multiple_days) { - do_storage(0); + day_do_storage(0); wins_update(FLAG_APP); } } @@ -765,6 +827,7 @@ int main(int argc, char **argv) wtimeout(win[KEY].p, -1); switch (key) { HANDLE_KEY(KEY_GENERIC_CHANGE_VIEW, key_generic_change_view); + HANDLE_KEY(KEY_GENERIC_PREV_VIEW, key_generic_prev_view); HANDLE_KEY(KEY_GENERIC_OTHER_CMD, key_generic_other_cmd); HANDLE_KEY(KEY_GENERIC_GOTO, key_generic_goto); HANDLE_KEY(KEY_GENERIC_GOTO_TODAY, key_generic_goto_today); diff --git a/src/calcurse.h b/src/calcurse.h index d65d088..8322416 100644 --- a/src/calcurse.h +++ b/src/calcurse.h @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -111,9 +111,6 @@ #define STATUSHEIGHT 2 #define MAX_NOTESIZ 40 -/* Format for appointment hours is: HH:MM */ -#define HRMIN_SIZE 6 - /* Maximum number of colors available. */ #define NBUSERCOLORS 6 @@ -142,12 +139,20 @@ */ #define DAYINSEC (DAYINMIN * MININSEC) #define NEXTDAY(date) date_sec_change((date), 0, 1) +#define PREVDAY(date) date_sec_change((date), 0, -1) #define DAYLEN(date) (NEXTDAY(date) - (date)) #define ENDOFDAY(date) (NEXTDAY(date) - 1) #define HOURINSEC (HOURINMIN * MININSEC) +#define DAY(date) (update_time_in_date(date, 0, 0)) /* Calendar window. */ #define CALHEIGHT 8 +/* + * Week day numbering (0, 1,..., 6) which depends on the first day of the week. + * The argument (d) is the "Sunday"-numbering of member tm_wday in struct tm. + */ +#define WDAY(d) \ + (modify_wday(d, -ui_calendar_get_wday_start())) /* Key definitions. */ #define CTRLVAL 0x1F @@ -285,7 +290,6 @@ struct conf { unsigned confirm_delete; enum win default_panel; unsigned compact_panels; - unsigned system_dialogs; unsigned multiple_days; unsigned header_line; unsigned event_separator; @@ -299,9 +303,11 @@ struct conf { int input_datefmt; /* format for reading date */ enum pos heading_pos; /* left/center/right for heading in appts panel */ char day_heading[BUFSIZ]; /* format for displaying heading in appts panel */ + char timefmt[BUFSIZ]; /* format for displaying time in appts panel*/ }; #define EMPTY_DAY_DEFAULT "--" +#define EMPTY_EVENT_DESC_DEFAULT _("(empty description)") /* Daemon-related configuration. */ struct dmon_conf { @@ -331,6 +337,9 @@ enum datefmt { /* Day heading default format. */ #define DAY_HEADING_DEFAULT "%B %e, %Y" +/* Appointment time default format. */ +#define APPT_TIME_DEFAULT "%H:%M" + /* * Calcurse representation of the date of a day in the calendar. * When time_t is a 32-bit signed integer, the year range is 1902 - 2037. @@ -379,40 +388,57 @@ struct excp { }; enum recur_type { - RECUR_NO, RECUR_DAILY, RECUR_WEEKLY, RECUR_MONTHLY, RECUR_YEARLY, - RECUR_TYPES + NBRECUR }; -/* To describe an item's repetition. */ +/* + * Recurrence rule according to RFC5545; used + * - in each recurrent appointment/event instance + * - in passing parameters as a single function argument + */ struct rpt { - enum recur_type type; /* repetition type */ - int freq; /* repetition frequency */ - time_t until; /* ending date for repeated event */ + enum recur_type type; /* FREQ */ + int freq; /* INTERVAL */ + time_t until; /* UNTIL */ + llist_t bymonth; /* BYMONTH list */ + llist_t bywday; /* BY(WEEK)DAY list */ + llist_t bymonthday; /* BYMONTHDAY list */ + llist_t exc; /* EXDATE's */ }; +/* Types of integers in rrule lists. */ +typedef enum { + BYMONTH, + BYDAY_W, + BYDAY_M, + BYDAY_Y, + BYMONTHDAY, + NOLL +} int_list_t; + /* Recurrent appointment definition. */ struct recur_apoint { - struct rpt *rpt; /* information about repetition */ - llist_t exc; /* days when the item should not be repeated */ - time_t start; /* beggining of the appointment */ - long dur; /* duration of the appointment */ - char state; /* 8 bits to store item state */ - char *mesg; /* appointment description */ - char *note; /* note attached to appointment */ + struct rpt *rpt; /* recurrence rule */ + llist_t exc; /* recurrence exceptions (NOT rpt->exc) */ + time_t start; /* start time */ + long dur; /* duration */ + char state; /* item state */ + char *mesg; /* description */ + char *note; /* attached note */ }; -/* Reccurent event definition. */ +/* Recurrent event definition. */ struct recur_event { - struct rpt *rpt; /* information about repetition */ - llist_t exc; /* days when the item should not be repeated */ + struct rpt *rpt; /* recurrence rule */ + llist_t exc; /* recurrence exceptions (NOT rpt->exc) */ int id; /* event type */ - time_t day; /* day at which event occurs */ - char *mesg; /* event description */ - char *note; /* note attached to event */ + time_t day; /* day of the event */ + char *mesg; /* description */ + char *note; /* attached note */ }; /* Generic pointer data type for appointments and events. */ @@ -488,11 +514,11 @@ struct notify_app { struct io_file { FILE *fd; - char name[BUFSIZ]; + char *name; }; -/* Available keys. */ -enum key { +/* Virtual keys. */ +enum vkey { KEY_GENERIC_CANCEL, KEY_GENERIC_SELECT, KEY_GENERIC_CREDITS, @@ -503,6 +529,7 @@ enum key { KEY_GENERIC_COPY, KEY_GENERIC_PASTE, KEY_GENERIC_CHANGE_VIEW, + KEY_GENERIC_PREV_VIEW, KEY_GENERIC_IMPORT, KEY_GENERIC_EXPORT, KEY_GENERIC_GOTO, @@ -542,7 +569,7 @@ enum key { KEY_RAISE_PRIORITY, KEY_LOWER_PRIORITY, - NBKEYS, + NBVKEYS, KEY_UNDEF, /* Non-configurable, context sensitive key bindings. */ @@ -647,7 +674,6 @@ struct nbar { char datefmt[BUFSIZ]; /* format for displaying date */ char timefmt[BUFSIZ]; /* format for displaying time */ char cmd[BUFSIZ]; /* notification command */ - const char *shell; /* user shell to launch notif. cmd */ unsigned notify_all; /* notify all appointments */ pthread_mutex_t mutex; }; @@ -762,7 +788,7 @@ void apoint_sec2str(struct apoint *, time_t, char *, char *); char *apoint_tostr(struct apoint *); char *apoint_hash(struct apoint *); void apoint_write(struct apoint *, FILE *); -struct apoint *apoint_scan(FILE *, struct tm, struct tm, char, char *, +char *apoint_scan(FILE *, struct tm, struct tm, char, char *, struct item_filter *); void apoint_delete(struct apoint *); struct notify_app *apoint_check_next(struct notify_app *, time_t); @@ -786,7 +812,7 @@ void ui_calendar_set_current_date(void); struct date *ui_calendar_get_today(void); void ui_calendar_set_first_day_of_week(enum wday); void ui_calendar_change_first_day_of_week(void); -unsigned ui_calendar_week_begins_on_monday(void); +int ui_calendar_get_wday_start(void); void ui_calendar_store_current_date(struct date *); void ui_calendar_init_slctd_day(void); struct date *ui_calendar_get_slctd_day(void); @@ -832,6 +858,7 @@ void day_display_item_date(struct day_item *, WINDOW *, int, time_t, int, int); void day_display_item(struct day_item *, WINDOW *, int, int, int, int); void day_write_stdout(time_t, const char *, const char *, const char *, const char *, int *); +void day_do_storage(int day_changed); void day_popup_item(struct day_item *); int day_check_if_item(struct date); unsigned day_chk_busy_slices(struct date, int, int *); @@ -860,7 +887,7 @@ unsigned event_inday(struct event *, time_t *); char *event_tostr(struct event *); char *event_hash(struct event *); void event_write(struct event *, FILE *); -struct event *event_scan(FILE *, struct tm, int, char *, struct item_filter *); +char *event_scan(FILE *, struct tm, int, char *, struct item_filter *); void event_delete(struct event *); void event_paste_item(struct event *, time_t); int event_dummy(struct day_item *); @@ -901,9 +928,8 @@ unsigned io_dir_exists(const char *); unsigned io_file_exists(const char *); int io_check_file(const char *); int io_check_data_files(void); -void io_startup_screen(int); void io_export_data(enum export_type, int); -void io_import_data(enum import_type, char *, const char *, const char *, +int io_import_data(enum import_type, char *, const char *, const char *, const char *, const char *, const char *); struct io_file *io_log_init(void); void io_log_print(struct io_file *, int, const char *); @@ -915,7 +941,6 @@ void io_set_lock(void); unsigned io_dump_pid(char *); unsigned io_get_pid(char *); int io_files_equal(const char *, const char *); -int io_file_is_empty(char *); int io_file_cp(const char *, const char *); void io_unset_modified(void); void io_set_modified(void); @@ -925,24 +950,26 @@ int io_get_modified(void); void keys_init(void); void keys_free(void); void keys_dump_defaults(char *); -const char *keys_get_label(enum key); -enum key keys_get_action(int); +const char *keys_get_label(enum vkey); +const char *keys_get_binding(enum vkey); +enum vkey keys_get_action(int); int keys_wgetch(WINDOW *); void keys_wait_for_any_key(WINDOW *); -enum key keys_get(WINDOW *, int *, int *); -int keys_assign_binding(int, enum key); -void keys_remove_binding(int, enum key); +enum vkey keys_get(WINDOW * win, int *, int *); +int keys_assign_binding(int, enum vkey); +void keys_remove_binding(int, enum vkey); int keys_str2int(const char *); char *keys_int2str(int); -int keys_action_count_keys(enum key); -const char *keys_action_firstkey(enum key); -const char *keys_action_nkey(enum key, int); -char *keys_action_allkeys(enum key); +int keys_action_count_keys(enum vkey); +const char *keys_action_firstkey(enum vkey); +const char *keys_action_nkey(enum vkey, int); +char *keys_action_allkeys(enum vkey); void keys_display_bindings_bar(WINDOW *, int *, int, int, int); -void keys_popup_info(enum key); +void keys_popup_info(enum vkey); void keys_save_bindings(FILE *); -int keys_check_missing_bindings(void); -void keys_fill_missing(void); +int keys_check_missing(void); +int keys_check_undefined(void); +int keys_fill_missing(void); /* listbox.c */ void listbox_init(struct listbox *, int, int, int, int, const char *, @@ -998,6 +1025,7 @@ void edit_note(char **, const char *); void view_note(const char *, const char *); void erase_note(char **); void note_read(char *, FILE *); +void note_read_contents(char *, size_t, FILE *); void note_gc(void); /* notify.c */ @@ -1029,7 +1057,11 @@ void pcal_export_data(FILE *); /* recur.c */ extern llist_ts_t recur_alist_p; extern llist_t recur_elist; -int recur_update_exc(llist_t *, char *); +void recur_free_int_list(llist_t *); +void recur_int_list_dup(llist_t *, llist_t *); +void recur_free_exc_list(llist_t *); +void recur_exc_dup(llist_t *, llist_t *); +int recur_str2exc(llist_t *, char *); char *recur_exc2str(llist_t *); struct recur_event *recur_event_dup(struct recur_event *); struct recur_apoint *recur_apoint_dup(struct recur_apoint *); @@ -1042,17 +1074,16 @@ void recur_event_llist_init(void); void recur_apoint_llist_free(void); void recur_event_llist_free(void); struct recur_apoint *recur_apoint_new(char *, char *, time_t, long, char, - int, int, time_t, llist_t *); -struct recur_event *recur_event_new(char *, char *, time_t, int, int, int, - time_t, llist_t *); + struct rpt *); +struct recur_event *recur_event_new(char *, char *, time_t, int, + struct rpt *); char recur_def2char(enum recur_type); int recur_char2def(char); -struct recur_apoint *recur_apoint_scan(FILE *, struct tm, struct tm, - char, int, struct tm, char *, - llist_t *, char, struct item_filter *); -struct recur_event *recur_event_scan(FILE *, struct tm, int, char, - int, struct tm, char *, llist_t *, - struct item_filter *); +char *recur_apoint_scan(FILE *, struct tm, struct tm, char, + char *, struct item_filter *, + struct rpt *); +char *recur_event_scan(FILE *, struct tm, int, char *, + struct item_filter *, struct rpt *); char *recur_apoint_tostr(struct recur_apoint *); char *recur_apoint_hash(struct recur_apoint *); void recur_apoint_write(struct recur_apoint *, FILE *); @@ -1060,22 +1091,29 @@ char *recur_event_tostr(struct recur_event *); char *recur_event_hash(struct recur_event *); void recur_event_write(struct recur_event *, FILE *); void recur_save_data(FILE *); -unsigned recur_item_find_occurrence(time_t, long, llist_t *, int, - int, time_t, time_t, time_t *); +unsigned recur_item_find_occurrence(time_t, long, struct rpt *, llist_t *, + time_t, time_t *); unsigned recur_apoint_find_occurrence(struct recur_apoint *, time_t, time_t *); unsigned recur_event_find_occurrence(struct recur_event *, time_t, time_t *); -unsigned recur_item_inday(time_t, long, llist_t *, int, int, time_t, time_t); +unsigned recur_item_inday(time_t, long, struct rpt *, llist_t *, time_t); unsigned recur_apoint_inday(struct recur_apoint *, time_t *); unsigned recur_event_inday(struct recur_event *, time_t *); void recur_event_add_exc(struct recur_event *, time_t); void recur_apoint_add_exc(struct recur_apoint *, time_t); void recur_event_erase(struct recur_event *); void recur_apoint_erase(struct recur_apoint *); +void recur_bymonth(llist_t *, FILE *); +void recur_bywday(enum recur_type, llist_t *, FILE *); +void recur_bymonthday(llist_t *, FILE *); void recur_exc_scan(llist_t *, FILE *); void recur_apoint_check_next(struct notify_app *, time_t, time_t); void recur_apoint_switch_notify(struct recur_apoint *); void recur_event_paste_item(struct recur_event *, time_t); void recur_apoint_paste_item(struct recur_apoint *, time_t); +int recur_next_occurrence(time_t, long, struct rpt *, llist_t *, time_t, time_t *); +int recur_nth_occurrence(time_t, long, struct rpt *, llist_t *, int, time_t *); +int recur_prev_occurrence(time_t, long, struct rpt *, llist_t *, time_t, time_t *); + /* sigs.c */ void sigs_init(void); @@ -1188,7 +1226,7 @@ int get_item_min(time_t); struct tm date2tm(struct date, unsigned, unsigned); time_t date2sec(struct date, unsigned, unsigned); struct date sec2date(time_t); -time_t utcdate2sec(struct date, unsigned, unsigned); +time_t tzdate2sec(struct date, unsigned, unsigned, char *); int date_cmp(struct date *, struct date *); int date_cmp_day(time_t, time_t); char *date_sec2date_str(time_t, const char *); @@ -1198,6 +1236,8 @@ time_t date_sec_change(time_t, int, int); time_t update_time_in_date(time_t, unsigned, unsigned); time_t get_sec_date(struct date); long min2sec(unsigned); +int modify_wday(int,int); +char *get_wday_default_string(int); void draw_scrollbar(struct scrollwin *, int); void item_in_popup(const char *, const char *, const char *, const char *); time_t get_today(void); @@ -1214,13 +1254,13 @@ int check_sec(time_t *); int check_time(unsigned, unsigned); int parse_time(const char *, unsigned *, unsigned *); int parse_duration(const char *, unsigned *, time_t); -int parse_date_duration(const char *, unsigned *, time_t); +int parse_date_increment(const char *, unsigned *, time_t); int parse_datetime(const char *, time_t *, time_t); void file_close(FILE *, const char *); void psleep(unsigned); -int fork_exec(int *, int *, const char *, const char *const *); -int shell_exec(int *, int *, const char *, const char *const *); -int child_wait(int *, int *, int); +int fork_exec(int *, int *, int *, int, const char *, const char *const *); +int shell_exec(int *, int *, int *, int, const char *, const char *const *); +int child_wait(int *, int *, int *, int); void press_any_key(void); void print_apoint(const char *, time_t, struct apoint *); void print_event(const char *, time_t, struct event *); @@ -1232,9 +1272,12 @@ int asprintf(char **, const char *, ...); int starts_with(const char *, const char *); int starts_with_ci(const char *, const char *); int hash_matches(const char *, const char *); -int show_dialogs(void); long overflow_add(long, long, long *); long overflow_mul(long, long, long *); +time_t next_wday(time_t, int); +int wday_per_year(int, int); +int wday_per_month(int, int, int); +char *day_ins(char **, time_t); /* vars.c */ extern int col, row; @@ -1290,6 +1333,7 @@ void wins_sbar_wdec(void); enum win wins_slctd(void); void wins_slctd_set(enum win); void wins_slctd_next(void); +void wins_slctd_prev(void); void wins_init(void); void wins_scrollwin_init(struct scrollwin *, int, int, int, int, const char *); void wins_scrollwin_resize(struct scrollwin *, int, int, int, int); diff --git a/src/config.c b/src/config.c index 27324e1..4e0f7db 100644 --- a/src/config.c +++ b/src/config.c @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -36,6 +36,8 @@ #include <ctype.h> #include <unistd.h> +#include <string.h> +#include <strings.h> #include "calcurse.h" @@ -109,6 +111,7 @@ static const struct confvar confmap[] = { {"format.inputdate", config_parse_input_datefmt, config_serialize_input_datefmt, NULL}, {"format.notifydate", CONFIG_HANDLER_STR(nbar.datefmt)}, {"format.notifytime", CONFIG_HANDLER_STR(nbar.timefmt)}, + {"format.appointmenttime", CONFIG_HANDLER_STR(conf.timefmt)}, {"format.outputdate", config_parse_output_datefmt, config_serialize_output_datefmt, NULL}, {"format.dayheading", CONFIG_HANDLER_STR(conf.day_heading)}, {"general.autogc", CONFIG_HANDLER_BOOL(conf.auto_gc)}, @@ -119,7 +122,6 @@ static const struct confvar confmap[] = { {"general.multipledays", CONFIG_HANDLER_BOOL(conf.multiple_days)}, {"general.periodicsave", CONFIG_HANDLER_UNSIGNED(conf.periodic_save)}, {"general.systemevents", CONFIG_HANDLER_BOOL(conf.systemevents)}, - {"general.systemdialogs", CONFIG_HANDLER_BOOL(conf.system_dialogs)}, {"notification.command", CONFIG_HANDLER_STR(nbar.cmd)}, {"notification.notifyall", config_parse_notifyall, config_serialize_notifyall, NULL}, {"notification.warning", CONFIG_HANDLER_INT(nbar.cntdwn)} @@ -261,14 +263,16 @@ static int config_parse_default_panel(void *dummy, const char *val) static int config_parse_first_day_of_week(void *dummy, const char *val) { - if (!strcmp(val, "monday")) - ui_calendar_set_first_day_of_week(MONDAY); - else if (!strcmp(val, "sunday")) - ui_calendar_set_first_day_of_week(SUNDAY); - else - return 0; + int i; - return 1; + for (i = 0; i < WEEKINDAYS; i++) { + if(!strcasecmp(val, get_wday_default_string(i))) { + ui_calendar_set_first_day_of_week(i); + return 1; + } + } + + return 0; } static int config_parse_color_theme(void *dummy, const char *val) @@ -468,10 +472,9 @@ static int config_serialize_default_panel(char **buf, void *dummy) static int config_serialize_first_day_of_week(char **buf, void *dummy) { - if (ui_calendar_week_begins_on_monday()) - *buf = mem_strdup("monday"); - else - *buf = mem_strdup("sunday"); + *buf = mem_strdup(get_wday_default_string(ui_calendar_get_wday_start())); + /* now stores string with uppercase first letter, changing to lower */ + **buf = tolower(**buf); return 1; } @@ -615,7 +618,8 @@ config_file_walk(config_fn_walk_cb_t fn_cb, * Backwards compatibility for removed configuration options: * ignored on load, omitted on save. */ - if (strcmp(key, "general.progressbar") == 0) + if (strcmp(key, "general.progressbar") == 0 || + strcmp(key, "general.systemdialogs") == 0) continue; if (value && (*value == '\0' || *value == '\n')) { diff --git a/src/custom.c b/src/custom.c index 575960a..2cd385c 100644 --- a/src/custom.c +++ b/src/custom.c @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -38,6 +38,7 @@ #include <stdlib.h> #include <math.h> #include <ctype.h> +#include <langinfo.h> #include "calcurse.h" @@ -540,12 +541,12 @@ enum { SYSTEM_EVENTS, CONFIRM_QUIT, CONFIRM_DELETE, - SYSTEM_DIAGS, FIRST_DAY_OF_WEEK, OUTPUT_DATE_FMT, INPUT_DATE_FMT, HEADING_POS, DAY_HEADING_FMT, + APPOINTMENT_TIME_FMT, NB_OPTIONS }; @@ -570,12 +571,12 @@ static void print_general_option(int i, WINDOW *win, int y, int hilt, void *cb_d "general.systemevents = ", "general.confirmquit = ", "general.confirmdelete = ", - "general.systemdialogs = ", "general.firstdayofweek = ", "format.outputdate = ", "format.inputdate = ", "appearance.headingposition = ", - "format.dayheading = " + "format.dayheading = ", + "format.appointmenttime = " }; const char *panel; const char *position; @@ -699,18 +700,10 @@ static void print_general_option(int i, WINDOW *win, int y, int hilt, void *cb_d _("(if set to YES, confirmation is required " "before deleting an event)")); break; - case SYSTEM_DIAGS: - print_bool_option_incolor(win, conf.system_dialogs, y, - XPOS + strlen(opt[SYSTEM_DIAGS])); - mvwaddstr(win, y + 1, XPOS, - _("(if set to YES, messages about loaded " - "and saved data will be displayed)")); - break; case FIRST_DAY_OF_WEEK: custom_apply_attr(win, ATTR_HIGHEST); mvwaddstr(win, y, XPOS + strlen(opt[FIRST_DAY_OF_WEEK]), - ui_calendar_week_begins_on_monday()? _("Monday") : - _("Sunday")); + nl_langinfo(DAY_1 + ui_calendar_get_wday_start())); custom_remove_attr(win, ATTR_HIGHEST); mvwaddstr(win, y + 1, XPOS, _("(specifies the first day of week in the calendar view)")); @@ -755,6 +748,14 @@ static void print_general_option(int i, WINDOW *win, int y, int hilt, void *cb_d mvwaddstr(win, y + 1, XPOS, _("(Format of the date displayed in the appointments panel)")); break; + case APPOINTMENT_TIME_FMT: + custom_apply_attr(win, ATTR_HIGHEST); + mvwaddstr(win, y, XPOS + strlen(opt[APPOINTMENT_TIME_FMT]), + conf.timefmt); + custom_remove_attr(win, ATTR_HIGHEST); + mvwaddstr(win, y + 1, XPOS, + _("(Format of the time displayed in the appointments panel)")); + break; } if (hilt) @@ -780,6 +781,8 @@ static void general_option_edit(int i) _("Enter a text string (an empty string for the default text)"); const char *output_datefmt_str = _("Enter the date format (see 'man 3 strftime' for possible formats) "); + const char *output_timefmt_str = + _("Enter the time format (see 'man 3 strftime' for possible formats) "); const char *input_datefmt_prefix = _("Enter the date format: "); const char *periodic_save_str = _("Enter the delay, in minutes, between automatic saves (0 to disable) "); @@ -873,9 +876,6 @@ static void general_option_edit(int i) case CONFIRM_DELETE: conf.confirm_delete = !conf.confirm_delete; break; - case SYSTEM_DIAGS: - conf.system_dialogs = !conf.system_dialogs; - break; case FIRST_DAY_OF_WEEK: ui_calendar_change_first_day_of_week(); ui_calendar_monthly_view_cache_set_invalid(); @@ -905,6 +905,15 @@ static void general_option_edit(int i) conf.day_heading[BUFSIZ - 1] = '\0'; } break; + case APPOINTMENT_TIME_FMT: + status_mesg(output_timefmt_str, ""); + strncpy(buf, conf.timefmt, BUFSIZ); + buf[BUFSIZ - 1] = '\0'; + if (updatestring(win[STA].p, &buf, 0, 1) == 0) { + strncpy(conf.timefmt, buf, BUFSIZ); + conf.timefmt[BUFSIZ - 1] = '\0'; + } + break; } mem_free(buf); @@ -987,10 +996,11 @@ print_keys_bindings(WINDOW * win, int selected_row, int selected_elm, const int XPOS = 1; const int EQUALPOS = 23; const int KEYPOS = 25; - int noelm, action, y; + int noelm, action, y, pos; + const char *key = NULL; noelm = y = 0; - for (action = 0; action < NBKEYS; action++) { + for (action = 0; action < NBVKEYS; action++) { char *actionstr; int nbkeys; @@ -1002,18 +1012,15 @@ print_keys_bindings(WINDOW * win, int selected_row, int selected_elm, mem_free(actionstr); mvwaddstr(win, y, EQUALPOS, "="); if (nbkeys == 0) - mvwaddstr(win, y, KEYPOS, _("undefined")); + mvwaddstr(win, y, KEYPOS, _("UNDEFINED")); if (action == selected_row) custom_remove_attr(win, ATTR_HIGHEST); if (nbkeys > 0) { if (action == selected_row) { - const char *key; - int pos; - + /* Elements may have been added or deleted. */ + wclrtoeol(win); pos = KEYPOS; - while ((key = - keys_action_nkey(action, - noelm)) != NULL) { + while ((key = keys_action_nkey(action, noelm))) { if (noelm == selected_elm) print_key_incolor(win, key, y, pos); @@ -1024,8 +1031,9 @@ print_keys_bindings(WINDOW * win, int selected_row, int selected_elm, pos += utf8_strwidth((char *)key) + 1; } } else { - mvwaddstr(win, y, KEYPOS, - keys_action_allkeys(action)); + key = keys_action_allkeys(action); + mvwaddstr(win, y, KEYPOS, key); + mem_free((char *)key); } } y += yoff; @@ -1057,9 +1065,11 @@ void custom_keys_config(void) const int LABELLINES = 3; clear(); - nbdisplayed = ((notify_bar() ? row - 3 : row - 2) - LABELLINES) / LINESPERKEY; - wins_scrollwin_init(&kwin, 0, 0, notify_bar() ? row - 3 : row - 2, col, _("keys configuration")); - wins_scrollwin_set_pad(&kwin, NBKEYS * LINESPERKEY); + nbdisplayed = ((notify_bar() ? row - 3 : row - 2) - + LABELLINES) / LINESPERKEY; + wins_scrollwin_init(&kwin, 0, 0, notify_bar() ? row - 3 : row - 2, col, + _("keys configuration")); + wins_scrollwin_set_pad(&kwin, NBVKEYS * LINESPERKEY); wins_scrollwin_draw_deco(&kwin, 0); custom_keys_config_bar(); selrow = selelm = 0; @@ -1084,7 +1094,7 @@ void custom_keys_config(void) } break; case KEY_MOVE_DOWN: - if (selrow < NBKEYS - 1) { + if (selrow < NBVKEYS - 1) { selrow++; selelm = 0; if (selrow == lastrow) { @@ -1115,7 +1125,7 @@ void custom_keys_config(void) keys_get_label(selrow), 0); for (;;) { ch = keys_wgetch(grabwin); - enum key action = keys_get_action(ch); + enum vkey action = keys_get_action(ch); /* Is the key already used by this action? */ if (action == selrow) break; @@ -1154,10 +1164,8 @@ void custom_keys_config(void) selelm--; break; case KEY_GENERIC_QUIT: - if (keys_check_missing_bindings() != 0) { - WARN_MSG(_("Some actions do not have any associated " - "key bindings!")); - } + if (keys_check_undefined()) + WARN_MSG(_("Some actions are left undefined!")); wins_scrollwin_delete(&kwin); return; } @@ -1213,7 +1221,10 @@ void custom_config_main(void) wmove(win[STA].p, 0, 0); wins_doupdate(); - while ((ch = keys_wgetch(win[KEY].p)) != 'q') { + while (1) { + ch = keys_wgetch(win[KEY].p); + if (keys_get_action(ch) == KEY_GENERIC_QUIT) + break; switch (ch) { case 'C': case 'c': @@ -1231,7 +1242,7 @@ void custom_config_main(void) old_layout = wins_layout(); custom_layout_config(); if (wins_layout() != old_layout) - wins_reset(); + wins_resize(); break; case 'G': case 'g': @@ -1257,7 +1268,11 @@ void custom_config_main(void) resize = 0; wins_reset(); } - + + /* needed to update app list */ + day_do_storage(0); + + /* wins_update(FLAG_ALL), but with custom bindings */ wins_set_bindings(bindings, ARRAY_SIZE(bindings)); wins_update_border(FLAG_ALL); wins_update_panels(FLAG_ALL); @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -58,7 +58,7 @@ static struct day_item sel_data = { 0, 0, 0, {NULL}}; /* * Save the item to become the selected APP item. - * Public function used to override the setting in do_storage(). + * Public function used to override the setting in day_do_storage(). */ int day_set_sel_data(struct day_item *d) { @@ -100,7 +100,7 @@ int day_sel_index(void) /* If still not found, stay on the same day. */ VECTOR_FOREACH(&day_items, i) { p = VECTOR_NTH(&day_items, i); - if (p->order == update_time_in_date(sel_data.order, 0, 0)) + if (p->order == DAY(sel_data.order)) return i; } return -1; @@ -198,7 +198,8 @@ static void day_add_item(int type, time_t start, time_t order, union aptev_ptr i /* Get the message of an item. */ char *day_item_get_mesg(struct day_item *day) { - switch (day->type) { + switch (day->type) + { case APPT: return day->item.apt->mesg; case EVNT: @@ -212,6 +213,15 @@ char *day_item_get_mesg(struct day_item *day) } } +/* Get the display message of an item. */ +char *day_item_get_display_mesg(struct day_item *day) +{ + char *msg = day_item_get_mesg(day); + if (msg[0] == '\0') + return EMPTY_EVENT_DESC_DEFAULT; + return msg; +} + /* Get the note attached to an item. */ char *day_item_get_note(struct day_item *day) { @@ -360,7 +370,7 @@ static int day_store_recur_events(time_t date) p.rev = rev; time_t occurrence; if (recur_event_find_occurrence(rev, date, &occurrence)) { - day_add_item(RECUR_EVNT, rev->day, occurrence, p); + day_add_item(RECUR_EVNT, occurrence, occurrence, p); e_nb++; } } @@ -529,7 +539,7 @@ day_display_item(struct day_item *day, WINDOW *win, int incolor, int width, if (width <= 0) return; - char *mesg = day_item_get_mesg(day); + char *mesg = day_item_get_display_mesg(day); ch_recur = (day->type == RECUR_EVNT) ? '*' : ' '; ch_note = day_item_get_note(day) ? '>' : ' '; @@ -578,12 +588,66 @@ void day_write_stdout(time_t date, const char *fmt_apt, const char *fmt_rapt, } } +/* + * Store events and appointments for a range of days in the day vector - + * beginning with the selected day - and load them into the APP listbox. If no + * day-change occurs, reset the selected APP item and with it the selected day, + * thereby storing and loading the same range of days. + */ +void day_do_storage(int day_changed) +{ + int pre_sel; + /* + * Save the selected item before rebuilding the day vector - + * unless a preselection is already set. + */ + if(!(pre_sel = day_check_sel_data())) + day_set_sel_data(ui_day_get_sel()); + + if (!day_changed) + ui_day_sel_reset(); + + /* The day_items vector. */ + day_store_items(get_slctd_day(), 1, day_get_days()); + /* The APP listbox. */ + ui_day_load_items(); + + if (day_changed && !pre_sel) + ui_day_sel_reset(); + else + ui_day_find_sel(); + + day_set_sel_data(&empty_day); +} + /* Display an item inside a popup window. */ void day_popup_item(struct day_item *day) { + const char *note_heading = _("Note:"); + size_t note_size = 3500; + if (day->type == EVNT || day->type == RECUR_EVNT) { - item_in_popup(NULL, NULL, day_item_get_mesg(day), - _("Event:")); + if (day_item_get_note(day)) { + char note[note_size]; + char *notepath, *msg; + FILE *fp; + + asprintf(¬epath, "%s%s", path_notes, day_item_get_note(day)); + fp = fopen(notepath, "r"); + if (fp == NULL) { + item_in_popup(NULL, NULL, day_item_get_mesg(day), _("Event:")); + return; + } + note_read_contents(note, note_size, fp); + fclose(fp); + mem_free(notepath); + + asprintf(&msg, "%s\n\n%s\n%s", day_item_get_display_mesg(day), note_heading, note); + item_in_popup(NULL, NULL, msg, _("Event:")); + mem_free(msg); + } else { + item_in_popup(NULL, NULL, day_item_get_display_mesg(day), _("Event:")); + } } else if (day->type == APPT || day->type == RECUR_APPT) { char a_st[100], a_end[100]; @@ -593,8 +657,28 @@ void day_popup_item(struct day_item *day) apt_tmp.start = day->start; apt_tmp.dur = day_item_get_duration(day); apoint_sec2str(&apt_tmp, ui_day_sel_date(), a_st, a_end); - item_in_popup(a_st, a_end, day_item_get_mesg(day), - _("Appointment:")); + + if (day_item_get_note(day)) { + char note[note_size]; + char *notepath, *msg; + FILE *fp; + + asprintf(¬epath, "%s%s", path_notes, day_item_get_note(day)); + fp = fopen(notepath, "r"); + if (fp == NULL) { + item_in_popup(a_st, a_end, day_item_get_mesg(day), _("Appointment:")); + return; + } + note_read_contents(note, note_size, fp); + fclose(fp); + mem_free(notepath); + + asprintf(&msg, "%s\n\n%s\n%s", day_item_get_display_mesg(day), note_heading, note); + item_in_popup(a_st, a_end, msg, _("Appointment:")); + mem_free(msg); + } else { + item_in_popup(a_st, a_end, day_item_get_display_mesg(day), _("Appointment:")); + } } else { EXIT(_("unknown item type")); /* NOTREACHED */ @@ -798,7 +882,7 @@ int day_paste_item(struct day_item *p, time_t date) /* wanted: until = shift + old_until */ if (p->item.rapt->rpt->until && overflow_add( - date - update_time_in_date(p->item.rapt->start, 0, 0), + date - DAY(p->item.rapt->start), p->item.rapt->rpt->until, &until) ) @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -34,7 +34,7 @@ * */ -#include <sys/types.h> +#include <sys/wait.h> #include <sys/stat.h> #include <unistd.h> #include <paths.h> @@ -143,8 +143,7 @@ static unsigned daemonize(int status) || !sigs_set_hdlr(SIGTERM, dmon_sigs_hdlr) || !sigs_set_hdlr(SIGALRM, dmon_sigs_hdlr) || !sigs_set_hdlr(SIGQUIT, dmon_sigs_hdlr) - || !sigs_set_hdlr(SIGUSR1, dmon_sigs_hdlr) - || !sigs_set_hdlr(SIGCHLD, SIG_IGN)) + || !sigs_set_hdlr(SIGUSR1, dmon_sigs_hdlr)) return 0; return 1; @@ -203,6 +202,9 @@ void dmon_start(int parent_exit_status) DMON_SLEEP_TIME); psleep(DMON_SLEEP_TIME); DMON_LOG(_("awakened at %s\n"), nowstr()); + /* Reap the user-defined notifications. */ + while (waitpid(0, NULL, WNOHANG) > 0) + ; } } diff --git a/src/event.c b/src/event.c index 375dd66..7c371e4 100644 --- a/src/event.c +++ b/src/event.c @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -149,7 +149,7 @@ void event_write(struct event *o, FILE * f) } /* Load the events from file */ -struct event *event_scan(FILE * f, struct tm start, int id, char *note, +char *event_scan(FILE * f, struct tm start, int id, char *note, struct item_filter *filter) { char buf[BUFSIZ], *nl; @@ -157,13 +157,13 @@ struct event *event_scan(FILE * f, struct tm start, int id, char *note, struct event *ev = NULL; int cond; - EXIT_IF(!check_date(start.tm_year, start.tm_mon, start.tm_mday) || - !check_time(start.tm_hour, start.tm_min), - _("date error in event")); + if (!check_date(start.tm_year, start.tm_mon, start.tm_mday) || + !check_time(start.tm_hour, start.tm_min)) + return _("illegal date in event"); /* Read the event description */ if (!fgets(buf, sizeof buf, f)) - return NULL; + return _("error in appointment description"); nl = strchr(buf, '\n'); if (nl) { @@ -177,8 +177,9 @@ struct event *event_scan(FILE * f, struct tm start, int id, char *note, start.tm_mon--; tstart = mktime(&start); - EXIT_IF(tstart == -1, _("date error in the event\n")); - tend = tstart + DAYINSEC - 1; + if (tstart == -1) + return _("date error in event\n"); + tend = ENDOFDAY(tstart); /* Filter item. */ if (filter) { @@ -205,8 +206,7 @@ struct event *event_scan(FILE * f, struct tm start, int id, char *note, } if (!ev) ev = event_new(buf, note, tstart, id); - - return ev; + return NULL; } /* Delete an event from the list. */ diff --git a/src/getstring.c b/src/getstring.c index 7cdb84e..8ea5df5 100644 --- a/src/getstring.c +++ b/src/getstring.c @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -241,9 +241,11 @@ enum getstr getstring(WINDOW * win, char *str, int l, int x, int y) st.len = st.pos; break; case CTRL('A'): /* go to beginning of string */ + case KEY_HOME: st.pos = 0; break; case CTRL('E'): /* go to end of string */ + case KEY_END: st.pos = st.len; break; case KEY_LEFT: /* move one char backward */ @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -105,7 +105,7 @@ int display_help(const char *topic) if (!io_file_exists(path)) { int ch = keys_str2int(topic); - enum key action = keys_get_action(ch); + enum vkey action = keys_get_action(ch); if (ch > 0 && action > 0 && action != KEY_UNDEF) { topic = keys_get_label(action); mem_free(path); @@ -134,6 +134,8 @@ int display_help(const char *topic) topic = "copy-paste"; else if (!strcmp(topic, "generic-change-view")) topic = "tab"; + else if (!strcmp(topic, "generic-prev-view")) + topic = "tab"; else if (!strcmp(topic, "generic-import")) topic = "import"; else if (!strcmp(topic, "generic-export")) diff --git a/src/hooks.c b/src/hooks.c index 20aed69..f649076 100644 --- a/src/hooks.c +++ b/src/hooks.c @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -35,35 +35,38 @@ */ #include <stddef.h> +#include <sys/wait.h> #include "calcurse.h" int run_hook(const char *name) { - char *hook_path = NULL; + char *hook_path = NULL, *mesg; + int pid, pin, pout, perr, ret = -127; char const *arg[2]; - int pid, ret = -127; - int prepare_wins = (ui_mode == UI_CURSES); asprintf(&hook_path, "%s/%s", path_hooks, name); - arg[0] = hook_path; - arg[1] = NULL; - if (!io_file_exists(hook_path)) goto cleanup; + arg[0] = hook_path; + arg[1] = NULL; - if (prepare_wins) - wins_prepare_external(); - - if ((pid = shell_exec(NULL, NULL, *arg, arg))) { - ret = child_wait(NULL, NULL, pid); - if (ret) - press_any_key(); + if ((pid = shell_exec(&pin, &pout, &perr, 1, *arg, arg))) { + ret = child_wait(&pin, &pout, &perr, pid); + if (ret > 0 && WIFEXITED(ret)) { + asprintf(&mesg, "%s hook: exit status %d", + name, + WEXITSTATUS(ret)); + que_ins(mesg, now(), 3); + mem_free(mesg); + } else if (ret != 0) { + asprintf(&mesg, "%s hook: abnormal termination", + name); + que_ins(mesg, now(), 4); + mem_free(mesg); + } } - if (prepare_wins) - wins_unprepare_external(); - cleanup: mem_free(hook_path); return ret; diff --git a/src/htable.h b/src/htable.h index 5266213..92be6e2 100644 --- a/src/htable.h +++ b/src/htable.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -36,11 +36,14 @@ #include <strings.h> #include <sys/types.h> +#include <ctype.h> #include "calcurse.h" #define ICALDATEFMT "%Y%m%d" #define ICALDATETIMEFMT "%Y%m%dT%H%M%S" +#define SEPARATOR "-- \n" +#define INDENT " " typedef enum { ICAL_VEVENT, @@ -54,12 +57,13 @@ typedef enum { EVENT } ical_vevent_e; -typedef struct { - enum recur_type type; - int freq; - long until; - unsigned count; -} ical_rpt_t; +typedef enum { + NO_PROPERTY, + SUMMARY, + DESCRIPTION, + LOCATION, + COMMENT +} ical_property_e; static void ical_export_header(FILE *); static void ical_export_recur_events(FILE *, int); @@ -69,25 +73,32 @@ static void ical_export_apoints(FILE *, int); static void ical_export_todo(FILE *, int); static void ical_export_footer(FILE *); -static const char *ical_recur_type[RECUR_TYPES] = - { "", "DAILY", "WEEKLY", "MONTHLY", "YEARLY" }; +static const char *ical_recur_type[NBRECUR] = + { "DAILY", "WEEKLY", "MONTHLY", "YEARLY" }; + +static const char *ical_wday[] = + {"SU", "MO", "TU", "WE", "TH", "FR", "SA"}; -/* Escape characters in field before printing */ -static void ical_format_line(FILE * stream, char * field, char * msg) +/* + * Encode a string as a property value of type TEXT (RFC 5545, 3.3.11). + */ +static void ical_format_line(FILE *stream, char *property, char *msg) { char * p; - fputs(field, stream); + fputs(property, stream); for (p = msg; *p; p++) { switch (*p) { + case '\n': + fprintf(stream, "\\%c", 'n'); + break; case ',': case ';': case '\\': fprintf(stream, "\\%c", *p); break; default: - fputc(*p, stream); - break; + fputc(*p, stream); } } fputc('\n', stream); @@ -104,12 +115,156 @@ static void ical_export_valarm(FILE * stream) fputs("END:VALARM\n", stream); } +static void ical_export_rrule(FILE *stream, struct rpt *rpt, ical_vevent_e item, + char *buf) +{ + llist_item_t *j; + int d; + char *fmt = item == EVENT ? ICALDATEFMT : + item == APPOINTMENT ? ICALDATETIMEFMT : + NULL; + + fprintf(stream, "RRULE:FREQ=%s", ical_recur_type[rpt->type]); + if (rpt->freq > 1) + fprintf(stream, ";INTERVAL=%d", rpt->freq); + if (rpt->until) { + date_sec2date_fmt(rpt->until, fmt, buf); + fprintf(stream, ";UNTIL=%s", buf); + } + if (LLIST_FIRST(&rpt->bymonth)) { + fputs(";BYMONTH=", stream); + LLIST_FOREACH(&rpt->bymonth, j) { + d = *(int *)LLIST_GET_DATA(j); + fprintf(stream, "%d", d); + if (LLIST_NEXT(j)) + fputc(',', stream); + } + } + if (LLIST_FIRST(&rpt->bywday)) { + int ord; + char sign; + + fputs(";BYDAY=", stream); + LLIST_FOREACH(&rpt->bywday, j) { + d = *(int *)LLIST_GET_DATA(j); + sign = d < 0 ? '-' : '+'; + d = abs(d); + ord = d / 7; + d = d % 7; + if (ord == 0) + fprintf(stream, "%s", ical_wday[d]); + else + fprintf(stream, "%c%d%s", sign, ord, ical_wday[d]); + if (LLIST_NEXT(j)) + fputc(',', stream); + } + } + if (LLIST_FIRST(&rpt->bymonthday)) { + fputs(";BYMONTHDAY=", stream); + LLIST_FOREACH(&rpt->bymonthday, j) { + d = *(int *)LLIST_GET_DATA(j); + fprintf(stream, "%d", d); + if (LLIST_NEXT(j)) + fputc(',', stream); + } + } + fputc('\n', stream); +} + +/* + * Copy the characters (lines) between "a" and "z" into an allocated string, + * un-indent it and return it. Note that the final character, a newline, is + * overwritten with '\0'. + */ +static char *ical_unindent(char *a, char *z) +{ + char *p, *q; int len; + + len = z - a + 1; + + p = mem_malloc(len); + strncpy(p, a, len); + p[len - 1] = '\0'; + while ((q = strstr(p, "\n" INDENT))) { + while (*(q + 1 + strlen(INDENT))) { + *(q + 1) = *(q + 1 + strlen(INDENT)); + q++; + } + *(q + 1) = '\0'; + } + return p; +} + +static void ical_export_note(FILE *stream, char *name) +{ + char *note_file, *p, *q, *r, *rest; + char *property[] = { + "Location: ", + "Comment: ", + NULL + }; + char *PROPERTY[] = { + "LOCATION:", + "COMMENT:" + }; + struct string note; + char lbuf[BUFSIZ]; + FILE *fp; + int has_desc, has_prop, i; + + asprintf(¬e_file, "%s/%s", path_notes, name); + if (!(fp = fopen(note_file, "r")) || ungetc(getc(fp), fp) == EOF) { + fclose(fp); + return; + } + string_init(¬e); + while (fgets(lbuf, BUFSIZ, fp)) + string_catf(¬e, "%s", lbuf); + fclose(fp); + + has_desc = has_prop = 0; + rest = note.buf; + if ((p = strstr(note.buf, SEPARATOR))) { + has_prop = 1; + rest = p + strlen(SEPARATOR); + if (p != note.buf) { + has_desc = 1; + *(--p) = '\0'; + } + } else { + has_desc = 1; + note.buf[strlen(note.buf) - 1] = '\0'; + } + + if (has_desc) + ical_format_line(stream, "DESCRIPTION:", note.buf); + + if (!has_prop) + goto cleanup; + for (i = 0; property[i]; i++) { + if ((p = strstr(rest, property[i]))) + p += strlen(property[i]); + else + continue; + /* Find end of property. */ + for (q = p; + (q = strchr(q, '\n')) && starts_with(q + 1, INDENT); + q++) ; + /* Extract property line(s). */ + r = ical_unindent(p, q); + ical_format_line(stream, PROPERTY[i], r); + mem_free(r); + } +cleanup: + mem_free(note.buf); +} + /* Export header. */ static void ical_export_header(FILE * stream) { fputs("BEGIN:VCALENDAR\n", stream); - fprintf(stream, "PRODID:-//calcurse//NONSGML v%s//EN\n", VERSION); fputs("VERSION:2.0\n", stream); + fprintf(stream, "PRODID:-//calcurse//NONSGML v%s//EN\n", VERSION); } /* Export footer. */ @@ -122,26 +277,21 @@ static void ical_export_footer(FILE * stream) static void ical_export_recur_events(FILE * stream, int export_uid) { llist_item_t *i, *j; - char ical_date[BUFSIZ]; + char ical_date[BUFSIZ], *hash; LLIST_FOREACH(&recur_elist, i) { struct recur_event *rev = LLIST_GET_DATA(i); - date_sec2date_fmt(rev->day, ICALDATEFMT, ical_date); fputs("BEGIN:VEVENT\n", stream); - fprintf(stream, "DTSTART:%s\n", ical_date); - fprintf(stream, "RRULE:FREQ=%s;INTERVAL=%d", - ical_recur_type[rev->rpt->type], rev->rpt->freq); - - if (rev->rpt->until != 0) { - date_sec2date_fmt(rev->rpt->until, ICALDATEFMT, - ical_date); - fprintf(stream, ";UNTIL=%s\n", ical_date); - } else { - fputc('\n', stream); + if (export_uid) { + hash = recur_event_hash(rev); + fprintf(stream, "UID:%s\n", hash); + mem_free(hash); } - + date_sec2date_fmt(rev->day, ICALDATEFMT, ical_date); + fprintf(stream, "DTSTART;VALUE=DATE:%s\n", ical_date); + ical_export_rrule(stream, rev->rpt, EVENT, ical_date); if (LLIST_FIRST(&rev->exc)) { - fputs("EXDATE:", stream); + fputs("EXDATE;VALUE=DATE:", stream); LLIST_FOREACH(&rev->exc, j) { struct excp *exc = LLIST_GET_DATA(j); date_sec2date_fmt(exc->st, ICALDATETIMEFMT, @@ -150,15 +300,9 @@ static void ical_export_recur_events(FILE * stream, int export_uid) fputc(LLIST_NEXT(j) ? ',' : '\n', stream); } } - ical_format_line(stream, "SUMMARY:", rev->mesg); - - if (export_uid) { - char *hash = recur_event_hash(rev); - fprintf(stream, "UID:%s\n", hash); - mem_free(hash); - } - + if (rev->note) + ical_export_note(stream, rev->note); fputs("END:VEVENT\n", stream); } } @@ -167,21 +311,21 @@ static void ical_export_recur_events(FILE * stream, int export_uid) static void ical_export_events(FILE * stream, int export_uid) { llist_item_t *i; - char ical_date[BUFSIZ]; + char ical_date[BUFSIZ], *hash; LLIST_FOREACH(&eventlist, i) { struct event *ev = LLIST_TS_GET_DATA(i); - date_sec2date_fmt(ev->day, ICALDATEFMT, ical_date); fputs("BEGIN:VEVENT\n", stream); - fprintf(stream, "DTSTART;VALUE=DATE:%s\n", ical_date); - ical_format_line(stream, "SUMMARY:", ev->mesg); - if (export_uid) { - char *hash = event_hash(ev); + hash = event_hash(ev); fprintf(stream, "UID:%s\n", hash); mem_free(hash); } - + date_sec2date_fmt(ev->day, ICALDATEFMT, ical_date); + fprintf(stream, "DTSTART;VALUE=DATE:%s\n", ical_date); + ical_format_line(stream, "SUMMARY:", ev->mesg); + if (ev->note) + ical_export_note(stream, ev->note); fputs("END:VEVENT\n", stream); } } @@ -190,16 +334,30 @@ static void ical_export_events(FILE * stream, int export_uid) static void ical_export_recur_apoints(FILE * stream, int export_uid) { llist_item_t *i, *j; - char ical_datetime[BUFSIZ]; - char ical_date[BUFSIZ]; + char ical_datetime[BUFSIZ], *hash; + time_t tod; LLIST_TS_LOCK(&recur_alist_p); LLIST_TS_FOREACH(&recur_alist_p, i) { struct recur_apoint *rapt = LLIST_TS_GET_DATA(i); + /* + * Add time-of-day to UNTIL/EXDATE. + * In calcurse until/exception is a date (midnight), but in + * RFC 5545 UNTIL/EXDATE is a DATE-TIME value type by default. + */ + tod = get_item_time(rapt->start); + if (rapt->rpt->until) + rapt->rpt->until += tod; + date_sec2date_fmt(rapt->start, ICALDATETIMEFMT, ical_datetime); fputs("BEGIN:VEVENT\n", stream); + if (export_uid) { + hash = recur_apoint_hash(rapt); + fprintf(stream, "UID:%s\n", hash); + mem_free(hash); + } fprintf(stream, "DTSTART:%s\n", ical_datetime); if (rapt->dur > 0) { fprintf(stream, "DURATION:P%ldDT%ldH%ldM%ldS\n", @@ -208,38 +366,22 @@ static void ical_export_recur_apoints(FILE * stream, int export_uid) (rapt->dur / MININSEC) % HOURINMIN, rapt->dur % MININSEC); } - fprintf(stream, "RRULE:FREQ=%s;INTERVAL=%d", - ical_recur_type[rapt->rpt->type], rapt->rpt->freq); - - if (rapt->rpt->until != 0) { - date_sec2date_fmt(rapt->rpt->until + HOURINSEC, - ICALDATEFMT, ical_date); - fprintf(stream, ";UNTIL=%s\n", ical_date); - } else { - fputc('\n', stream); - } - + ical_export_rrule(stream, rapt->rpt, APPOINTMENT, ical_datetime); if (LLIST_FIRST(&rapt->exc)) { fputs("EXDATE:", stream); LLIST_FOREACH(&rapt->exc, j) { struct excp *exc = LLIST_GET_DATA(j); - date_sec2date_fmt(exc->st, ICALDATETIMEFMT, - ical_date); - fprintf(stream, "%s", ical_date); + date_sec2date_fmt(exc->st + tod, ICALDATETIMEFMT, + ical_datetime); + fprintf(stream, "%s", ical_datetime); fputc(LLIST_NEXT(j) ? ',' : '\n', stream); } } - ical_format_line(stream, "SUMMARY:", rapt->mesg); + if (rapt->note) + ical_export_note(stream, rapt->note); if (rapt->state & APOINT_NOTIFY) ical_export_valarm(stream); - - if (export_uid) { - char *hash = recur_apoint_hash(rapt); - fprintf(stream, "UID:%s\n", hash); - mem_free(hash); - } - fputs("END:VEVENT\n", stream); } LLIST_TS_UNLOCK(&recur_alist_p); @@ -249,14 +391,19 @@ static void ical_export_recur_apoints(FILE * stream, int export_uid) static void ical_export_apoints(FILE * stream, int export_uid) { llist_item_t *i; - char ical_datetime[BUFSIZ]; + char ical_datetime[BUFSIZ], *hash; LLIST_TS_LOCK(&alist_p); LLIST_TS_FOREACH(&alist_p, i) { struct apoint *apt = LLIST_TS_GET_DATA(i); + fputs("BEGIN:VEVENT\n", stream); + if (export_uid) { + hash = apoint_hash(apt); + fprintf(stream, "UID:%s\n", hash); + mem_free(hash); + } date_sec2date_fmt(apt->start, ICALDATETIMEFMT, ical_datetime); - fputs("BEGIN:VEVENT\n", stream); fprintf(stream, "DTSTART:%s\n", ical_datetime); if (apt->dur > 0) { fprintf(stream, "DURATION:P%ldDT%ldH%ldM%ldS\n", @@ -266,15 +413,10 @@ static void ical_export_apoints(FILE * stream, int export_uid) apt->dur % MININSEC); } ical_format_line(stream, "SUMMARY:", apt->mesg); + if (apt->note) + ical_export_note(stream, apt->note); if (apt->state & APOINT_NOTIFY) ical_export_valarm(stream); - - if (export_uid) { - char *hash = apoint_hash(apt); - fprintf(stream, "UID:%s\n", hash); - mem_free(hash); - } - fputs("END:VEVENT\n", stream); } LLIST_TS_UNLOCK(&alist_p); @@ -284,22 +426,23 @@ static void ical_export_apoints(FILE * stream, int export_uid) static void ical_export_todo(FILE * stream, int export_uid) { llist_item_t *i; + char *hash; LLIST_FOREACH(&todolist, i) { struct todo *todo = LLIST_TS_GET_DATA(i); fputs("BEGIN:VTODO\n", stream); - if (todo->completed) - fprintf(stream, "STATUS:COMPLETED\n"); - fprintf(stream, "PRIORITY:%d\n", todo->id); - ical_format_line(stream, "SUMMARY:", todo->mesg); - if (export_uid) { - char *hash = todo_hash(todo); + hash = todo_hash(todo); fprintf(stream, "UID:%s\n", hash); mem_free(hash); } - + fprintf(stream, "PRIORITY:%d\n", todo->id); + ical_format_line(stream, "SUMMARY:", todo->mesg); + if (todo->note) + ical_export_note(stream, todo->note); + if (todo->completed) + fprintf(stream, "STATUS:COMPLETED\n"); fputs("END:VTODO\n", stream); } } @@ -364,25 +507,37 @@ static void ical_store_todo(int priority, int completed, char *mesg, erase_note(¬e); } +/* + * Calcurse limitation: events are one-day (all-day), and all multi-day events + * are turned into one-day events; a note has been added by ical_read_event(). + */ static void -ical_store_event(char *mesg, char *note, long day, long end, - ical_rpt_t * rpt, llist_t * exc, const char *fmt_ev, +ical_store_event(char *mesg, char *note, time_t day, time_t end, + struct rpt *rpt, llist_t *exc, const char *fmt_ev, const char *fmt_rev) { const int EVENTID = 1; struct event *ev; struct recur_event *rev; + if (!mesg) + mesg = mem_strdup(_("(empty)")); + EXIT_IF(!mesg, _("ical_store_event: out of memory")); + + /* + * Repeating event. The end day is ignored, and the event becomes + * one-day even if multi-day. + */ if (rpt) { - rev = recur_event_new(mesg, note, day, EVENTID, rpt->type, - rpt->freq, rpt->until, exc); - mem_free(rpt); + rpt->exc = *exc; + rev = recur_event_new(mesg, note, day, EVENTID, rpt); if (fmt_rev) print_recur_event(fmt_rev, day, rev); goto cleanup; } - if (end == 0 || end - day <= DAYINSEC) { + /* Ordinary one-day event. */ + if (end - day <= DAYINSEC) { ev = event_new(mesg, note, day, EVENTID); if (fmt_ev) print_event(fmt_ev, day, ev); @@ -390,21 +545,19 @@ ical_store_event(char *mesg, char *note, long day, long end, } /* - * Here we have an event that spans over several days. - * - * In iCal, the end specifies when the event is supposed to end, in - * calcurse, the end specifies the time that the last occurrence of the - * event starts, so we need to do some conversion here. + * Ordinary multi-day event. The event is turned into a daily repeating + * event until the day before the end. In iCal, the end day is + * exclusive, the until day inclusive. */ - end = day + ((end - day - 1) / DAYINSEC) * DAYINSEC; - rpt = mem_malloc(sizeof(ical_rpt_t)); - rpt->type = RECUR_DAILY; - rpt->freq = 1; - rpt->count = 0; - rpt->until = end; - rev = recur_event_new(mesg, note, day, EVENTID, rpt->type, - rpt->freq, rpt->until, exc); - mem_free(rpt); + struct rpt tmp; + tmp.type = RECUR_DAILY; + tmp.freq = 1; + tmp.until = day + ((end - day - 1) / DAYINSEC) * DAYINSEC; + LLIST_INIT(&tmp.bymonth); + LLIST_INIT(&tmp.bywday); + LLIST_INIT(&tmp.bymonthday); + tmp.exc = *exc; + rev = recur_event_new(mesg, note, day, EVENTID, &tmp); if (fmt_rev) print_recur_event(fmt_rev, day, rev); @@ -414,20 +567,41 @@ cleanup: } static void -ical_store_apoint(char *mesg, char *note, long start, long dur, - ical_rpt_t * rpt, llist_t * exc, int has_alarm, +ical_store_apoint(char *mesg, char *note, time_t start, long dur, + struct rpt *rpt, llist_t *exc, int has_alarm, const char *fmt_apt, const char *fmt_rapt) { char state = 0L; struct apoint *apt; struct recur_apoint *rapt; + time_t day; + + if (!mesg) + mesg = mem_strdup(_("(empty)")); + EXIT_IF(!mesg, _("ical_store_event: out of memory")); if (has_alarm) state |= APOINT_NOTIFY; if (rpt) { - rapt = recur_apoint_new(mesg, note, start, dur, state, - rpt->type, rpt->freq, rpt->until, exc); - mem_free(rpt); + /* + * In calcurse, "until" is interpreted as a day (DATE) - hours, + * minutes and seconds are ignored - whereas in iCal the full + * DATE-TIME value of "until" is taken into account. It follows + * that if the event in calcurse has an occurrence on the until + * day, and the start time is after the until value, the + * calcurse until day must be changed to the day before. + */ + if (rpt->until) { + day = DAY(rpt->until); + if (recur_item_find_occurrence(start, dur, rpt, NULL, + day, NULL) && + get_item_time(rpt->until) < get_item_time(start)) + rpt->until = date_sec_change(day, 0, -1); + else + rpt->until = day; + } + rpt->exc = *exc; + rapt = recur_apoint_new(mesg, note, start, dur, state, rpt); if (fmt_rapt) print_recur_apoint(fmt_rapt, start, rapt->start, rapt); } else { @@ -440,11 +614,12 @@ ical_store_apoint(char *mesg, char *note, long start, long dur, } /* - * Returns an allocated string representing the argument string with escaped - * characters decoded, or NULL on error. - * The string is assumed to be the value part of a SUMMARY or DESCRIPTION line. + * Return an allocated string containing the decoded 'line' or NULL on error. + * The last arguments are used to format a note file entry. + * The line is assumed to be the value part of a content line of type TEXT or + * INTEGER (RFC 5545, 3.3.11 and 3.3.8) without list or field separators (3.1.1). */ -static char *ical_unformat_line(char *line) +static char *ical_unformat_line(char *line, int eol, int indentation) { struct string s; char *p; @@ -457,6 +632,8 @@ static char *ical_unformat_line(char *line) case 'N': case 'n': string_catf(&s, "%c", '\n'); + if (indentation) + string_catf(&s, "%s", INDENT); p++; break; case '\\': @@ -472,9 +649,7 @@ static char *ical_unformat_line(char *line) break; case ',': case ';': - /* - * No list or field separator allowed. - */ + /* No list or field separator allowed. */ mem_free(s.buf); return NULL; default: @@ -482,6 +657,9 @@ static char *ical_unformat_line(char *line) break; } } + /* Add the final EOL removed by ical_readline(). */ + if (eol) + string_catf(&s, "\n"); return string_buf(&s); } @@ -550,39 +728,83 @@ ical_chk_header(FILE * fd, char *buf, char *lstore, unsigned *lineno, } /* + * Return the TZID property parameter value from a DTSTART/DTEND/EXDATE property + * in an allocated string. The value may be any text string not containing the + * characters '"', ';', ':' and ',' (RFC 5545, sections 3.2.19 and 3.1). + */ +static char *ical_get_tzid(char *p) +{ + const char param[] = ";TZID="; + char *q; + int s; + + if (!(p = strstr(p, param))) + return NULL; + p += sizeof(param) - 1; + if (*p == '"') + return NULL; + + q = strpbrk(p, ":;"); + s = q - p + 1; + q = mem_malloc(s); + strncpy(q, p, s); + q[s - 1] = '\0'; + + return q; +} + +/* + * Return event type from a DTSTART/DTEND/EXDATE property. + */ +static ical_vevent_e ical_get_type(char *c_line) +{ + const char vparam[] = ";VALUE=DATE"; + char *p; + + if ((p = strstr(c_line, vparam))) { + p += sizeof(vparam) - 1; + if (*p == ':' || *p == ';') + return EVENT; + } + + return APPOINTMENT; +} + +/* * iCalendar date-time format is based on the ISO 8601 complete * representation. It should be something like : DATE 'T' TIME * where DATE is 'YYYYMMDD' and TIME is 'HHMMSS'. * The time and 'T' separator are optional (in the case of an day-long event). * - * Optionally, if the type pointer is given, specify if it is an event - * (no time is given, meaning it is an all-day event), or an appointment - * (time is given). - * - * The timezone is not yet handled by calcurse. + * The type argument is either APPOINTMENT or EVENT, and the time format must + * match (either DATE-TIME or DATE). The time zone identifier is ignored in an + * EVENT or in an APPOINTMENT with UTC time. */ -static time_t ical_datetime2time_t(char *datestr, ical_vevent_e * type) +static time_t ical_datetime2time_t(char *datestr, char *tzid, ical_vevent_e type) { - const int FORMAT_DATE = 3, FORMAT_DATETIME = 6, FORMAT_DATETIMEZ = 7; + const int INVALID = 0, DATE = 3, DATETIME = 6, DATETIMEZ = 7; struct date date; unsigned hour, min, sec; - char c; + char c, UTC[] = ""; int format; + EXIT_IF(type == UNDEFINED, "event type not set"); + format = sscanf(datestr, "%04u%02u%02uT%02u%02u%02u%c", &date.yyyy, &date.mm, &date.dd, &hour, &min, &sec, &c); - if (format == FORMAT_DATE) { - if (type) - *type = EVENT; + + if (format == DATE && strlen(datestr) > 8) + format = INVALID; + if (format == DATETIMEZ && c != 'Z') + format = DATETIME; + + if (format == DATE && type == EVENT) return date2sec(date, 0, 0); - } else if (format == FORMAT_DATETIME || format == FORMAT_DATETIMEZ) { - if (type) - *type = APPOINTMENT; - if (format == FORMAT_DATETIMEZ && c == 'Z') - return utcdate2sec(date, hour, min); - else - return date2sec(date, hour, min); - } + else if (format == DATETIME && type == APPOINTMENT) + return tzdate2sec(date, hour, min, tzid); + else if (format == DATETIMEZ && type == APPOINTMENT) + return tzdate2sec(date, hour, min, UTC); + return 0; } @@ -616,15 +838,22 @@ static long ical_durtime2long(char *timestr) } /* - * Extract from RFC2445: + * Extract from RFC2445 section 3.8.2.5: + * + * Property Name: DURATION + * + * Purpose: This property specifies a positive duration of time. + * + * Value Type: DURATION + * + * and section 3.3.6: * * Value Name: DURATION * * Purpose: This value type is used to identify properties that contain - * duration of time. + * a duration of time. * - * Formal Definition: The value type is defined by the following - * notation: + * Format Definition: The value type is defined by the following notation: * * dur-value = (["+"] / "-") "P" (dur-date / dur-time / dur-week) * dur-date = dur-day [dur-time] @@ -635,73 +864,41 @@ static long ical_durtime2long(char *timestr) * dur-second = 1*DIGIT "S" * dur-day = 1*DIGIT "D" * + * For events, duration must be days or weeks. * Example: A duration of 15 days, 5 hours and 20 seconds would be: * P15DT5H0M20S * A duration of 7 weeks would be: * P7W */ -static long ical_dur2long(char *durstr) +static long ical_dur2long(char *durstr, ical_vevent_e type) { - char *p; + char *p = durstr, c; int bytes_read; - struct { - unsigned week, day; - } date; - - memset(&date, 0, sizeof date); - - p = strchr(durstr, 'P'); - if (!p) - return -1; - p++; + unsigned week, day; if (*p == '-') - return -1; + return 0; if (*p == '+') p++; + if (*p != 'P') + return 0; - if (*p == 'T') { + p++; + if (*p == 'T' && type == APPOINTMENT) /* dur-time */ return ical_durtime2long(p); - } else if (strchr(p, 'W')) { + else if (sscanf(p, "%u%c", &week, &c) == 2 && c == 'W') /* dur-week */ - if (sscanf(p, "%u", &date.week) == 1) - return date.week * WEEKINDAYS * DAYINSEC; - } else if (strchr(p, 'D')) { + return week * WEEKINDAYS * DAYINSEC; + else if (sscanf(p, "%u%c%n", &day, &c, &bytes_read) == 2 && c == 'D') { /* dur-date */ - if (sscanf(p, "%uD%n", &date.day, &bytes_read) == 1) { - p += bytes_read; - return date.day * DAYINSEC + ical_durtime2long(p); - } + p += bytes_read; + return day * DAYINSEC + (*p == 'T' && type == APPOINTMENT ? + ical_durtime2long(p) : + 0); } - return -1; -} - -/* - * Compute the vevent repetition end date from the repetition count. - * - * Extract from RFC2445: - * The COUNT rule part defines the number of occurrences at which to - * range-bound the recurrence. The "DTSTART" property value, if specified, - * counts as the first occurrence. - */ -static long ical_compute_rpt_until(long start, ical_rpt_t * rpt) -{ - switch (rpt->type) { - case RECUR_DAILY: - return date_sec_change(start, 0, rpt->freq * (rpt->count - 1)); - case RECUR_WEEKLY: - return date_sec_change(start, 0, - rpt->freq * WEEKINDAYS * (rpt->count - 1)); - case RECUR_MONTHLY: - return date_sec_change(start, rpt->freq * (rpt->count - 1), 0); - case RECUR_YEARLY: - return date_sec_change(start, - rpt->freq * 12 * (rpt->count - 1), 0); - default: - return 0; - } + return 0; } /* @@ -713,8 +910,8 @@ static char *ical_get_value(char *p) return NULL; for (; *p != ':'; p++) { if (*p == '"') - for (p++; *p != '"' && *p != '\0'; p++); - if (*p == '\0') + for (p++; *p && *p != '"'; p++); + if (!*p) return NULL; } @@ -722,51 +919,159 @@ static char *ical_get_value(char *p) } /* + * Fill in the bymonth linked list from a comma-separated list of + * unsigned integers terminated by a space or end of string. + */ +static int ical_bymonth(llist_t *ll, char *cl) +{ + unsigned mon; + int *i, n; + + while (!(*cl == ' ' || *cl == '\0')) { + if (!(sscanf(cl, "%u%n", &mon, &n) == 1)) + return 0; + i = mem_malloc(sizeof(int)); + *i = mon; + LLIST_ADD(ll, i); + cl += n; + cl += (*cl == ','); + } + return 1; +} + +/* + * Fill in the bymonthday linked list from a comma-separated list of + * (signed) integers terminated by a space or end of string. + */ +static int ical_bymonthday(llist_t *ll, char *cl) +{ + int mday; + int *i, n; + + while (!(*cl == ' ' || *cl == '\0')) { + if (!(sscanf(cl, "%d%n", &mday, &n) == 1)) + return 0; + i = mem_malloc(sizeof(int)); + *i = mday; + LLIST_ADD(ll, i); + cl += n; + cl += (*cl == ','); + } + return 1; +} + +/* + * Fill in the bywday linked list from a comma-separated list of (ordered) + * weekday names (+1SU, MO, -5SA, 25TU, etc.) terminated by a space or end of + * string. + */ +static int ical_bywday(llist_t *ll, char *cl) +{ + int sign, order, wday, n, *i; + char *owd; + + while (!(*cl == ' ' || *cl == '\0')) { + /* find list separator */ + for (owd = cl; !(*cl == ',' || *cl == ' ' || *cl == '\0'); cl++) + ; + cl += (*cl == ','); + + if (!(sscanf(owd, "%d%n", &order, &n) == 1)) + order = n = 0; + sign = (order < 0) ? -1 : 1; + order *= sign; + owd += n; + if (starts_with(owd, "SU")) + wday = 0; + else if (starts_with(owd, "MO")) + wday = 1; + else if (starts_with(owd, "TU")) + wday = 2; + else if (starts_with(owd, "WE")) + wday = 3; + else if (starts_with(owd, "TH")) + wday = 4; + else if (starts_with(owd, "FR")) + wday = 5; + else if (starts_with(owd, "SA")) + wday = 6; + else + return 0; + + wday = sign * (wday + order * WEEKINDAYS); + i = mem_malloc(sizeof(int)); + *i = wday; + LLIST_ADD(ll, i); + } + return 1; +} + +/* * Read a recurrence rule from an iCalendar RRULE string. * + * RFC 5545, section 3.8.5.3: + * + * Property Name: RRULE + * + * Purpose: This property defines a rule or repeating pattern for + * recurring events, to-dos, journal entries, or time zone definitions. + * + * Value Type: RECUR + * + * RFC 5545, section 3.3.10: + * * Value Name: RECUR * * Purpose: This value type is used to identify properties that contain * a recurrence rule specification. * - * Formal Definition: The value type is defined by the following + * Format Definition: The value type is defined by the following * notation: * - * recur = "FREQ"=freq *( - * - * ; either UNTIL or COUNT may appear in a 'recur', - * ; but UNTIL and COUNT MUST NOT occur in the same 'recur' - * - * ( ";" "UNTIL" "=" enddate ) / - * ( ";" "COUNT" "=" 1*DIGIT ) / - * - * ; the rest of these keywords are optional, - * ; but MUST NOT occur more than - * ; once + * recur = recur-rule-part *( ";" recur-rule-part ) + * ; + * ; The rule parts are not ordered in any particular sequence. + * ; + * ; The FREQ rule part is REQUIRED, + * ; but MUST NOT occur more than once. + * ; + * ; The UNTIL or COUNT rule parts are OPTIONAL, + * ; but they MUST NOT occur in the same 'recur'. + * ; + * ; The other rule parts are OPTIONAL, + * ; but MUST NOT occur more than once. * - * ( ";" "INTERVAL" "=" 1*DIGIT ) / - * ( ";" "BYSECOND" "=" byseclist ) / - * ( ";" "BYMINUTE" "=" byminlist ) / - * ( ";" "BYHOUR" "=" byhrlist ) / - * ( ";" "BYDAY" "=" bywdaylist ) / - * ( ";" "BYMONTHDAY" "=" bymodaylist ) / - * ( ";" "BYYEARDAY" "=" byyrdaylist ) / - * ( ";" "BYWEEKNO" "=" bywknolist ) / - * ( ";" "BYMONTH" "=" bymolist ) / - * ( ";" "BYSETPOS" "=" bysplist ) / - * ( ";" "WKST" "=" weekday ) / - * ( ";" x-name "=" text ) - * ) -*/ -static ical_rpt_t *ical_read_rrule(FILE * log, char *rrulestr, - unsigned *noskipped, const int itemline) + * recur-rule-part = ( "FREQ"=freq ) + * / ( "UNTIL" "=" enddate ) + * / ( "COUNT" "=" 1*DIGIT ) + * / ( "INTERVAL" "=" 1*DIGIT ) + * / ( "BYSECOND" "=" byseclist ) + * / ( "BYMINUTE" "=" byminlist ) + * / ( "BYHOUR" "=" byhrlist ) + * / ( "BYDAY" "=" bywdaylist ) + * / ( "BYMONTHDAY" "=" bymodaylist ) + * / ( "BYYEARDAY" "=" byyrdaylist ) + * / ( "BYWEEKNO" "=" bywknolist ) + * / ( "BYMONTH" "=" bymolist ) + * / ( "BYSETPOS" "=" bysplist ) + * / ( "WKST" "=" weekday ) + */ +static struct rpt *ical_read_rrule(FILE *log, char *rrulestr, + unsigned *noskipped, + const int itemline, + ical_vevent_e type, + time_t start, + int *count) { - const char count[] = "COUNT="; - const char interv[] = "INTERVAL="; - char freqstr[BUFSIZ]; - unsigned interval; - ical_rpt_t *rpt; - char *p; + char freqstr[8], datestr[17]; + struct rpt *rpt; + char *p, *q; + + if (type == UNDEFINED) { + ical_log(log, ICAL_VEVENT, itemline, + _("need DTSTART to determine event type.")); + return NULL; + } p = ical_get_value(rrulestr); if (!p) { @@ -775,72 +1080,143 @@ static ical_rpt_t *ical_read_rrule(FILE * log, char *rrulestr, (*noskipped)++; return NULL; } - - rpt = mem_malloc(sizeof(ical_rpt_t)); - memset(rpt, 0, sizeof(ical_rpt_t)); - if (sscanf(p, "FREQ=%s", freqstr) != 1) { + /* Prepare for scanf(): replace semicolons by spaces. */ + for (q = p; (q = strchr(q, ';')); *q = ' ', q++) + ; + + rpt = mem_malloc(sizeof(struct rpt)); + memset(rpt, 0, sizeof(struct rpt)); + LLIST_INIT(&rpt->bymonth); + LLIST_INIT(&rpt->bywday); + LLIST_INIT(&rpt->bymonthday); + + /* FREQ rule part */ + if ((p = strstr(rrulestr, "FREQ="))) { + if (sscanf(p, "FREQ=%7s", freqstr) != 1) { + ical_log(log, ICAL_VEVENT, itemline, + _("frequency not set in rrule.")); + (*noskipped)++; + mem_free(rpt); + return NULL; + } + } else { ical_log(log, ICAL_VEVENT, itemline, - _("recurrence frequency not found.")); + _("frequency absent in rrule.")); (*noskipped)++; mem_free(rpt); return NULL; } - if (starts_with(freqstr, "DAILY")) { + if (!strcmp(freqstr, "DAILY")) rpt->type = RECUR_DAILY; - } else if (starts_with(freqstr, "WEEKLY")) { + else if (!strcmp(freqstr, "WEEKLY")) rpt->type = RECUR_WEEKLY; - } else if (starts_with(freqstr, "MONTHLY")) { + else if (!strcmp(freqstr, "MONTHLY")) rpt->type = RECUR_MONTHLY; - } else if (starts_with(freqstr, "YEARLY")) { + else if (!strcmp(freqstr, "YEARLY")) rpt->type = RECUR_YEARLY; - } else { + else { + ical_log(log, ICAL_VEVENT, itemline, + _("rrule frequency not supported.")); + (*noskipped)++; + mem_free(rpt); + return NULL; + } + + /* INTERVAL rule part */ + rpt->freq = 1; + if ((p = strstr(rrulestr, "INTERVAL="))) { + if (sscanf(p, "INTERVAL=%d", &rpt->freq) != 1) { + ical_log(log, ICAL_VEVENT, itemline, _("invalid interval.")); + (*noskipped)++; + mem_free(rpt); + return NULL; + } + } + + /* UNTIL and COUNT rule parts */ + if (strstr(rrulestr, "UNTIL=") && strstr(rrulestr, "COUNT=")) { ical_log(log, ICAL_VEVENT, itemline, - _("recurrence frequency not recognized.")); + _("either until or count.")); (*noskipped)++; mem_free(rpt); return NULL; } + if ((p = strstr(rrulestr, "UNTIL="))) { + if (sscanf(p, "UNTIL=%16s", datestr) != 1) { + ical_log(log, ICAL_VEVENT, itemline, + _("missing until value.")); + (*noskipped)++; + mem_free(rpt); + return NULL; + } + rpt->until = ical_datetime2time_t(datestr, NULL, type); + if (!rpt->until) { + ical_log(log, ICAL_VEVENT, itemline, + _("invalid until format.")); + (*noskipped)++; + mem_free(rpt); + return NULL; + } + } + /* - * The UNTIL rule part defines a date-time value which bounds the - * recurrence rule in an inclusive manner. If not present, and the - * COUNT rule part is also not present, the RRULE is considered to - * repeat forever. - - * The COUNT rule part defines the number of occurrences at which to - * range-bound the recurrence. The "DTSTART" property value, if - * specified, counts as the first occurrence. + * COUNT is converted to UNTIL in ical_read_event() once all recurrence + * parameters are known. */ - if ((p = strstr(rrulestr, "UNTIL")) != NULL) { - rpt->until = ical_datetime2time_t(strchr(p, '=') + 1, NULL); - } else { - unsigned cnt; - char *countstr; - - rpt->until = 0; - if ((countstr = strstr(rrulestr, count))) { - countstr += sizeof(count) - 1; - if (sscanf(countstr, "%u", &cnt) == 1) - rpt->count = cnt; + if ((p = strstr(rrulestr, "COUNT="))) { + p = strchr(p, '=') + 1; + if (!(sscanf(p, "%d", count) == 1 && *count)) { + ical_log(log, ICAL_VEVENT, itemline, + _("invalid count value.")); + (*noskipped)++; + mem_free(rpt); + return NULL; } } - rpt->freq = 1; - if ((p = strstr(rrulestr, interv))) { - p += sizeof(interv) - 1; - if (sscanf(p, "%u", &interval) == 1) - rpt->freq = interval; + /* BYMONTH rule part */ + if ((p = strstr(rrulestr, "BYMONTH="))) { + p = strchr(p, '=') + 1; + if (!ical_bymonth(&rpt->bymonth, p)) { + ical_log(log, ICAL_VEVENT, itemline, + _("invalid bymonth list.")); + (*noskipped)++; + mem_free(rpt); + return NULL; + } + } + + /* BYMONTHDAY rule part */ + if ((p = strstr(rrulestr, "BYMONTHDAY="))) { + p = strchr(p, '=') + 1; + if (!ical_bymonthday(&rpt->bymonthday, p)) { + ical_log(log, ICAL_VEVENT, itemline, + _("invalid bymonthday list.")); + (*noskipped)++; + mem_free(rpt); + return NULL; + } + } + + /* BYDAY rule part */ + if ((p = strstr(rrulestr, "BYDAY="))) { + p = strchr(p, '=') + 1; + if (!ical_bywday(&rpt->bywday, p)) { + ical_log(log, ICAL_VEVENT, itemline, + _("invalid byday list.")); + (*noskipped)++; + mem_free(rpt); + return NULL; + } } return rpt; } -static void ical_add_exc(llist_t * exc_head, long date) +static void ical_add_exc(llist_t * exc_head, time_t date) { - if (date == 0) - return; - struct excp *exc = mem_malloc(sizeof(struct excp)); exc->st = date; @@ -848,67 +1224,96 @@ static void ical_add_exc(llist_t * exc_head, long date) } /* - * This property defines the list of date/time exceptions for a + * This property defines a comma-separated list of date/time exceptions for a * recurring calendar component. */ static int -ical_read_exdate(llist_t * exc, FILE * log, char *exstr, - unsigned *noskipped, const int itemline) +ical_read_exdate(llist_t * exc, FILE * log, char *exstr, unsigned *noskipped, + const int itemline, ical_vevent_e type) { - char *p, *q; + char *p, *q, *tzid = NULL; + time_t t; + int n; + if (type != ical_get_type(exstr)) { + ical_log(log, ICAL_VEVENT, itemline, + _("invalid exception date value type.")); + goto cleanup; + } p = ical_get_value(exstr); if (!p) { ical_log(log, ICAL_VEVENT, itemline, _("malformed exceptions line.")); - (*noskipped)++; - return 0; + goto cleanup; } - - while ((q = strchr(p, ',')) != NULL) { - char buf[BUFSIZ]; - const int buflen = q - p; - - strncpy(buf, p, buflen); - buf[buflen] = '\0'; - ical_add_exc(exc, ical_datetime2time_t(buf, NULL)); - p = ++q; + tzid = ical_get_tzid(exstr); + /* Count the exceptions and replace commas by zeroes */ + for (q = p, n = 1; (q = strchr(q, ',')); *q = '\0', q++, n++) + ; + while (n) { + if (!(t = ical_datetime2time_t(p, tzid, type))) { + ical_log(log, ICAL_VEVENT, itemline, + _("invalid exception.")); + goto cleanup; + } + ical_add_exc(exc, t); + p = strchr(p, '\0') + 1; + n--; } - ical_add_exc(exc, ical_datetime2time_t(p, NULL)); - return 1; + +cleanup: + (*noskipped)++; + if (tzid) + mem_free(tzid); + return 0; } -/* Return an allocated string containing the name of the newly created note. */ -static char *ical_read_note(char *line, unsigned *noskipped, +/* + * Return an allocated string containing a property value to be written in a + * note file or NULL on error. + */ +static char *ical_read_note(char *line, ical_property_e property, unsigned *noskipped, ical_types_e item_type, const int itemline, FILE * log) { - char *p, *notestr, *note; + const int EOL = 1, + IND = (property != DESCRIPTION); + char *p, *pname, *notestr; + + switch (property) { + case DESCRIPTION: + pname = "description"; + break; + case LOCATION: + pname = "location"; + break; + case COMMENT: + pname = "comment"; + break; + default: + pname = "no property"; + } p = ical_get_value(line); if (!p) { - ical_log(log, item_type, itemline, - _("malformed description line.")); + asprintf(&p, _("malformed %s line."), pname); + ical_log(log, item_type, itemline, p); + mem_free(p); (*noskipped)++; - return NULL; + notestr = NULL; + goto leave; } - notestr = ical_unformat_line(p); + notestr = ical_unformat_line(p, EOL, IND); if (!notestr) { - ical_log(log, item_type, itemline, _("malformed description.")); + asprintf(&p, _("malformed %s."), pname); + ical_log(log, item_type, itemline, p); + mem_free(p); (*noskipped)++; - return NULL; - } else if (strlen(notestr) == 0) { - mem_free(notestr); - ical_log(log, item_type, itemline, _("empty description.")); - (*noskipped)++; - return NULL; - } else { - note = generate_note(notestr); - mem_free(notestr); - return note; } + leave: + return notestr; } /* Returns an allocated string containing the ical item summary. */ @@ -916,30 +1321,28 @@ static char *ical_read_summary(char *line, unsigned *noskipped, ical_types_e item_type, const int itemline, FILE * log) { - char *p, *summary; + const int EOL = 0, IND = 0; + char *p, *summary = NULL; p = ical_get_value(line); if (!p) { - ical_log(log, item_type, itemline, _("malformed summary line")); + ical_log(log, item_type, itemline, _("malformed summary line.")); (*noskipped)++; - return NULL; + goto leave; } - summary = ical_unformat_line(p); + summary = ical_unformat_line(p, EOL, IND); if (!summary) { ical_log(log, item_type, itemline, _("malformed summary.")); (*noskipped)++; - return NULL; - } - - /* Event summaries must not contain newlines. */ - if (strchr(summary, '\n')) { - ical_log(log, item_type, itemline, _("line break in summary.")); - (*noskipped)++; - mem_free(summary); - return NULL; + goto leave; } + /* An event summary is one line only. */ + for (p = summary; *p; p++) + if (*p == '\n') + *p = ' '; + leave: return summary; } @@ -951,21 +1354,29 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents, { const int ITEMLINE = *lineno - !feof(fdi); ical_vevent_e vevent_type; - char *p; + ical_property_e property; + char *p, *note, *tzid; + char *dtstart, *dtend, *duration, *rrule; + struct string s, exdate; struct { llist_t exc; - ical_rpt_t *rpt; - char *mesg, *note; - long start, end, dur; + struct rpt *rpt; + int count; + char *mesg, *desc, *loc, *comm, *imp, *note; + time_t start, end; + long dur; int has_alarm; } vevent; - int skip_alarm; + int skip_alarm, has_note, separator, has_exdate; vevent_type = UNDEFINED; memset(&vevent, 0, sizeof vevent); LLIST_INIT(&vevent.exc); - skip_alarm = 0; + note = dtstart = dtend = duration = rrule = NULL; + skip_alarm = has_note = separator = has_exdate =0; while (ical_readline(fdi, buf, lstore, lineno)) { + note = NULL; + property = NO_PROPERTY; if (skip_alarm) { /* * Need to skip VALARM properties because some keywords @@ -975,51 +1386,213 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents, skip_alarm = 0; continue; } - if (starts_with_ci(buf, "END:VEVENT")) { - if (!vevent.mesg) { + /* DTSTART and related properties (picked up earlier). */ + if (!dtstart) { ical_log(log, ICAL_VEVENT, ITEMLINE, - _("could not retrieve item summary.")); + _("item start date not defined.")); goto skip; } - if (vevent.start == 0) { + vevent_type = ical_get_type(dtstart); + if ((tzid = ical_get_tzid(dtstart)) && + vevent_type == APPOINTMENT) { + if (vevent.imp) { + asprintf(&p, "%s, TZID=%s", + vevent.imp, tzid); + mem_free(vevent.imp); + vevent.imp = p; + } else + asprintf(&vevent.imp, "TZID=%s", tzid); + has_note = separator = 1; + } + p = ical_get_value(dtstart); + if (!p) { ical_log(log, ICAL_VEVENT, ITEMLINE, - _("item start date is not defined.")); + _("malformed start time line.")); goto skip; } - - if (vevent_type == APPOINTMENT && vevent.dur == 0) { - if (vevent.end != 0) { - vevent.dur = vevent.end - vevent.start; + vevent.start = ical_datetime2time_t(p, tzid, vevent_type); + if (tzid) { + mem_free(tzid); + tzid = NULL; + } + if (!vevent.start) { + ical_log(log, ICAL_VEVENT, ITEMLINE, + _("invalid or malformed event " + "start time.")); + goto skip; + } + /* DTEND */ + if (!dtend) + goto duration; + if (vevent_type != ical_get_type(dtend)) { + ical_log(log, ICAL_VEVENT, ITEMLINE, + _("invalid end time value type.")); + goto skip; + } + tzid = ical_get_tzid(dtend); + p = ical_get_value(dtend); + if (!p) { + ical_log(log, ICAL_VEVENT, ITEMLINE, + _("malformed end time line.")); + goto skip; + } + vevent.end = ical_datetime2time_t(p, tzid, vevent_type); + if (tzid) { + mem_free(tzid); + tzid = NULL; + } + if (!vevent.end) { + ical_log(log, ICAL_VEVENT, ITEMLINE, + _("malformed event end time.")); + goto skip; + } + if (vevent.end <= vevent.start) { + ical_log(log, ICAL_VEVENT, ITEMLINE, + _("end must be later than start.")); + goto skip; + } + duration: + if (!duration) + goto rrule; + if (vevent.end) { + ical_log(log, ICAL_VEVENT, ITEMLINE, + _("either end or duration.")); + goto skip; + } + p = ical_get_value(duration); + if (!p) { + ical_log(log, ICAL_VEVENT, ITEMLINE, + _("malformed duration line.")); + goto skip; + } + vevent.dur = ical_dur2long(p, vevent_type); + if (!vevent.dur) { + ical_log(log, ICAL_VEVENT, ITEMLINE, + _("invalid duration.")); + goto skip; + } + rrule: + if (!rrule) + goto exdate; + vevent.rpt = ical_read_rrule(log, rrule, noskipped, + ITEMLINE, vevent_type, vevent.start, + &vevent.count); + if (!vevent.rpt) + goto cleanup; + exdate: + if (!has_exdate) + goto duration_end; + if (!rrule) { + ical_log(log, ICAL_VEVENT, ITEMLINE, + _("exception date, but no recurrence " + "rule.")); + goto skip; + } + if (!ical_read_exdate(&vevent.exc, log, exdate.buf, + noskipped, ITEMLINE, vevent_type)) + goto cleanup; + duration_end: + /* An APPOINTMENT must always have a duration. */ + if (vevent_type == APPOINTMENT && !vevent.dur) { + vevent.dur = vevent.end ? + vevent.end - vevent.start : + 0; + } + /* An EVENT must always have an end. */ + if (vevent_type == EVENT) { + if (!vevent.end) + vevent.end = vevent.start + vevent.dur; + vevent.dur = vevent.end - vevent.start; + if (vevent.dur > DAYINSEC) { + /* Add note on multi-day events. */ + char *md = _("multi-day event changed " + "to one-day event"); + if (vevent.imp) { + asprintf(&p, "%s, %s", + vevent.imp, md); + mem_free(vevent.imp); + vevent.imp = p; + } else + asprintf(&vevent.imp, "%s", md); + has_note = separator = 1; } - - if (vevent.dur < 0) { - ical_log(log, ICAL_VEVENT, ITEMLINE, - _("item has a negative duration.")); - goto skip; + } + if (has_note) { + /* Construct string with note file contents. */ + string_init(&s); + if (vevent.desc) { + string_catf(&s, "%s", vevent.desc); + mem_free(vevent.desc); + vevent.desc = NULL; } + if (separator) + string_catf(&s, SEPARATOR); + if (vevent.loc) { + string_catf(&s, _("Location: %s"), + vevent.loc); + mem_free(vevent.loc); + vevent.loc = NULL; + } + if (vevent.comm) { + string_catf(&s, _("Comment: %s"), + vevent.comm); + mem_free(vevent.comm); + vevent.comm = NULL; + } + if (vevent.imp) { + string_catf(&s, ("Import: %s\n"), + vevent.imp); + mem_free(vevent.imp); + vevent.imp = NULL; + } + vevent.note = generate_note(string_buf(&s)); + mem_free(s.buf); } - - if (vevent.rpt && vevent.rpt->count) { - vevent.rpt->until = - ical_compute_rpt_until(vevent.start, - vevent.rpt); + if (vevent.rpt) { + time_t day, until; + long dur; + char *msg; + + dur = vevent_type == EVENT ? -1 : vevent.dur; + day = DAY(vevent.start); + msg = _("rrule does not match start day (%s)."); + + if (vevent.count) { + recur_nth_occurrence(vevent.start, + dur, + vevent.rpt, + &vevent.exc, + vevent.count, + &until); + vevent.rpt->until = until; + } + if (!recur_item_find_occurrence(vevent.start, + dur, + vevent.rpt, + NULL, + day, + NULL)) { + char *l = day_ins(&msg, vevent.start); + ical_log(log, ICAL_VEVENT, ITEMLINE, l); + mem_free(l); + goto skip; + } } - switch (vevent_type) { case APPOINTMENT: ical_store_apoint(vevent.mesg, vevent.note, - vevent.start, vevent.dur, - vevent.rpt, &vevent.exc, - vevent.has_alarm, fmt_apt, - fmt_rapt); + vevent.start, vevent.dur, + vevent.rpt, &vevent.exc, + vevent.has_alarm, + fmt_apt, fmt_rapt); (*noapoints)++; break; case EVENT: ical_store_event(vevent.mesg, vevent.note, - vevent.start, vevent.end, - vevent.rpt, &vevent.exc, - fmt_ev, fmt_rev); + vevent.start, vevent.end, + vevent.rpt, &vevent.exc, + fmt_ev, fmt_rev); (*noevents)++; break; case UNDEFINED: @@ -1028,54 +1601,32 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents, goto skip; break; } - return; } - if (starts_with_ci(buf, "DTSTART")) { - p = ical_get_value(buf); - if (!p) { - ical_log(log, ICAL_VEVENT, ITEMLINE, - _("malformed start time line.")); - goto skip; - } - - vevent.start = ical_datetime2time_t(p, &vevent_type); - if (!vevent.start) { - ical_log(log, ICAL_VEVENT, ITEMLINE, - _("could not retrieve event start time.")); - goto skip; - } + /* + * DTSTART has a value type: either DATE-TIME or DATE. + * In calcurse DATE-TIME implies an appointment, DATE an + * event. + * Properties DTEND, DURATION and EXDATE and rrule part + * UNTIL must match the DTSTART value type. + */ + asprintf(&dtstart, "%s", buf); } else if (starts_with_ci(buf, "DTEND")) { - p = ical_get_value(buf); - if (!p) { - ical_log(log, ICAL_VEVENT, ITEMLINE, - _("malformed end time line.")); - goto skip; - } - - vevent.end = ical_datetime2time_t(p, &vevent_type); - if (!vevent.end) { - ical_log(log, ICAL_VEVENT, ITEMLINE, - _("could not retrieve event end time.")); - goto skip; - } + asprintf(&dtend, "%s", buf); } else if (starts_with_ci(buf, "DURATION")) { - vevent.dur = ical_dur2long(buf); - if (vevent.dur <= 0) { - ical_log(log, ICAL_VEVENT, ITEMLINE, - _("item duration malformed.")); - goto skip; - } + asprintf(&duration, "%s", buf); } else if (starts_with_ci(buf, "RRULE")) { - vevent.rpt = ical_read_rrule(log, buf, noskipped, - ITEMLINE); - if (!vevent.rpt) - goto cleanup; + asprintf(&rrule, "%s", buf); } else if (starts_with_ci(buf, "EXDATE")) { - if (!ical_read_exdate(&vevent.exc, log, buf, noskipped, - ITEMLINE)) - goto cleanup; + if (!has_exdate) { + has_exdate = 1; + string_init(&exdate); + string_catf(&exdate, "%s", buf); + } else { + p = ical_get_value(buf); + string_catf(&exdate, ",%s", p); + } } else if (starts_with_ci(buf, "SUMMARY")) { vevent.mesg = ical_read_summary(buf, noskipped, ICAL_VEVENT, ITEMLINE, log); @@ -1084,23 +1635,78 @@ ical_read_event(FILE * fdi, FILE * log, unsigned *noevents, } else if (starts_with_ci(buf, "BEGIN:VALARM")) { skip_alarm = vevent.has_alarm = 1; } else if (starts_with_ci(buf, "DESCRIPTION")) { - vevent.note = ical_read_note(buf, noskipped, - ICAL_VEVENT, ITEMLINE, log); - if (!vevent.note) + property = DESCRIPTION; + } else if (starts_with_ci(buf, "LOCATION")) { + property = LOCATION; + } else if (starts_with_ci(buf, "COMMENT")) { + property = COMMENT; + } + if (property) { + note = ical_read_note(buf, property, noskipped, + ICAL_VEVENT, ITEMLINE, log); + if (!note) goto cleanup; + if (!separator) + separator = (property != DESCRIPTION); + has_note = 1; + } + switch (property) { + case DESCRIPTION: + if (vevent.desc) { + ical_log(log, ICAL_VEVENT, ITEMLINE, + _("only one description allowed.")); + goto skip; + } + vevent.desc = note; + break; + case LOCATION: + if (vevent.loc) { + ical_log(log, ICAL_VEVENT, ITEMLINE, + _("only one location allowed.")); + goto skip; + } + vevent.loc = note; + break; + case COMMENT: + /* There may be more than one. */ + if (vevent.comm) { + asprintf(&p, "%sComment: %s", + vevent.comm, note); + mem_free(vevent.comm); + vevent.comm = p; + } else + vevent.comm = note; + break; + default: + break; } } - ical_log(log, ICAL_VEVENT, ITEMLINE, _("The ical file seems to be malformed. " "The end of item was not found.")); - -skip: + skip: (*noskipped)++; - cleanup: - if (vevent.note) - mem_free(vevent.note); + if (dtstart) + mem_free(dtstart); + if (dtend) + mem_free(dtend); + if (duration) + mem_free(duration); + if (rrule) + mem_free(rrule); + if (has_exdate) + mem_free(exdate.buf); + if (note) + mem_free(note); + if (vevent.desc) + mem_free(vevent.desc); + if (vevent.loc) + mem_free(vevent.loc); + if (vevent.comm) + mem_free(vevent.comm); + if (vevent.imp) + mem_free(vevent.imp); if (vevent.mesg) mem_free(vevent.mesg); if (vevent.rpt) @@ -1113,16 +1719,22 @@ ical_read_todo(FILE * fdi, FILE * log, unsigned *notodos, unsigned *noskipped, char *buf, char *lstore, unsigned *lineno, const char *fmt_todo) { const int ITEMLINE = *lineno - !feof(fdi); + ical_property_e property; + char *p, *note; + struct string s; struct { - char *mesg, *note; + char *mesg, *desc, *loc, *comm, *note; int priority; int completed; } vtodo; - int skip_alarm; + int skip_alarm, has_note, separator; memset(&vtodo, 0, sizeof vtodo); - skip_alarm = 0; + note = NULL; + skip_alarm = has_note = separator = 0; while (ical_readline(fdi, buf, lstore, lineno)) { + note = NULL; + property = NO_PROPERTY; if (skip_alarm) { /* * Need to skip VALARM properties because some keywords @@ -1132,20 +1744,42 @@ ical_read_todo(FILE * fdi, FILE * log, unsigned *notodos, unsigned *noskipped, skip_alarm = 0; continue; } - if (starts_with_ci(buf, "END:VTODO")) { if (!vtodo.mesg) { ical_log(log, ICAL_VTODO, ITEMLINE, _("could not retrieve item summary.")); goto cleanup; } - + if (has_note) { + /* Construct string with note file contents. */ + string_init(&s); + if (vtodo.desc) { + string_catf(&s, "%s", vtodo.desc); + mem_free(vtodo.desc); + vtodo.desc = NULL; + } + if (separator) + string_catf(&s, SEPARATOR); + if (vtodo.loc) { + string_catf(&s, _("Location: %s"), + vtodo.loc); + mem_free(vtodo.loc); + vtodo.loc = NULL; + } + if (vtodo.comm) { + string_catf(&s, _("Comment: %s"), + vtodo.comm); + mem_free(vtodo.comm); + vtodo.comm = NULL; + } + vtodo.note = generate_note(string_buf(&s)); + mem_free(s.buf); + } ical_store_todo(vtodo.priority, vtodo.completed, vtodo.mesg, vtodo.note, fmt_todo); (*notodos)++; return; } - if (starts_with_ci(buf, "PRIORITY:")) { sscanf(buf, "PRIORITY:%d\n", &vtodo.priority); if (vtodo.priority < 0 || vtodo.priority > 9) { @@ -1165,22 +1799,66 @@ ical_read_todo(FILE * fdi, FILE * log, unsigned *notodos, unsigned *noskipped, } else if (starts_with_ci(buf, "BEGIN:VALARM")) { skip_alarm = 1; } else if (starts_with_ci(buf, "DESCRIPTION")) { - vtodo.note = ical_read_note(buf, noskipped, ICAL_VTODO, - ITEMLINE, log); - if (!vtodo.note) + property = DESCRIPTION; + } else if (starts_with_ci(buf, "LOCATION")) { + property = LOCATION; + } else if (starts_with_ci(buf, "COMMENT")) { + property = COMMENT; + } + if (property) { + note = ical_read_note(buf, property, noskipped, + ICAL_VTODO, ITEMLINE, log); + if (!note) goto cleanup; + if (!separator) + separator = (property != DESCRIPTION); + has_note = 1; + } + switch (property) { + case DESCRIPTION: + if (vtodo.desc) { + ical_log(log, ICAL_VTODO, ITEMLINE, + _("only one description allowed.")); + goto skip; + } + vtodo.desc = note; + break; + case LOCATION: + if (vtodo.loc) { + ical_log(log, ICAL_VTODO, ITEMLINE, + _("only one location allowed.")); + goto skip; + } + vtodo.loc = note; + break; + case COMMENT: + /* There may be more than one. */ + if (vtodo.comm) { + asprintf(&p, "%sComment: %s", + vtodo.comm, note); + mem_free(vtodo.comm); + vtodo.comm = p; + } else + vtodo.comm = note; + break; + default: + break; } } - ical_log(log, ICAL_VTODO, ITEMLINE, _("The ical file seems to be malformed. " "The end of item was not found.")); - -skip: + skip: (*noskipped)++; cleanup: - if (vtodo.note) - mem_free(vtodo.note); + if (note) + mem_free(note); + if (vtodo.desc) + mem_free(vtodo.desc); + if (vtodo.loc) + mem_free(vtodo.loc); + if (vtodo.comm) + mem_free(vtodo.comm); if (vtodo.mesg) mem_free(vtodo.mesg); } @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -50,8 +50,8 @@ struct ht_keybindings_s { const char *label; - enum key key; - HTABLE_ENTRY(ht_keybindings_s); + enum vkey key; + HTABLE_ENTRY(ht_keybindings_s); }; static void load_keys_ht_getkey(struct ht_keybindings_s *, const char **, @@ -237,25 +237,25 @@ void io_dump_apts(const char *fmt_apt, const char *fmt_rapt, LLIST_FOREACH(&recur_elist, i) { struct recur_event *rev = LLIST_GET_DATA(i); - time_t day = update_time_in_date(rev->day, 0, 0); + 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 = update_time_in_date(rapt->start, 0, 0); + 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 = update_time_in_date(apt->start, 0, 0); + 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 = update_time_in_date(ev->day, 0, 0); + time_t day = DAY(ev->day); print_event(fmt_ev, day, ev); } } @@ -553,13 +553,13 @@ 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; - llist_t exc; + struct rpt rpt; time_t t; int id = 0; - int freq; char type, state = 0L; char note[MAX_NOTESIZ + 1], *notep; unsigned line = 0; + char *scan_error; t = time(NULL); localtime_r(&t, <); @@ -572,9 +572,10 @@ void io_load_app(struct item_filter *filter) rewind(data_file); for (;;) { - LLIST_INIT(&exc); is_appointment = is_event = is_recursive = 0; line++; + scan_error = NULL; + c = getc(data_file); if (c == EOF) break; @@ -630,102 +631,113 @@ void io_load_app(struct item_filter *filter) if (c == '{') { is_recursive = 1; - if (fscanf(data_file, " %d%c ", &freq, &type) != 2) + 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); - if (c == '}') { /* endless recurrent item */ - until.tm_year = 0; - while ((c = getc(data_file)) == ' ') ; - ungetc(c, data_file); - } else if (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 item repetition")); + _("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); - if (c == '!') { - ungetc(c, data_file); - recur_exc_scan(&exc, data_file); - while ((c = - getc(data_file)) == ' ') ; - ungetc(c, data_file); - } else if (c == '}') { - while ((c = - getc(data_file)) == ' ') ; - ungetc(c, data_file); - } else { + } else + rpt.until = 0; + /* Optional bymonthday list */ + if (c == 'd') { + if (rpt.type == RECUR_WEEKLY) io_load_error(path_apts, line, - _("syntax error in item repetition")); - } - } else if (c == '!') { /* endless item with exceptions */ + _("BYMONTHDAY illegal with WEEKLY")); ungetc(c, data_file); - recur_exc_scan(&exc, data_file); - while ((c = getc(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); - until.tm_year = 0; - } else { + 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, - _("wrong format in the appointment or event")); - /* NOTREACHED */ - } - } else { - ungetc(c, data_file); + _("missing end of recurrence")); + while ((c = getc(data_file)) == ' ') ; } /* Check if a note is attached to the item. */ - c = getc(data_file); if (c == '>') { note_read(note, data_file); + c = getc(data_file); notep = note; - } else { + } else notep = NULL; - ungetc(c, data_file); - } /* * Last: read the item description and load it into its * corresponding linked list, depending on the item type. */ if (is_appointment) { - c = getc(data_file); - if (c == '!') { + if (c == '!') state |= APOINT_NOTIFY; - while ((c = getc(data_file)) == ' ') ; - ungetc(c, data_file); - } else if (c == '|') { + else if (c == '|') state = 0L; - while ((c = getc(data_file)) == ' ') ; - ungetc(c, data_file); - } else { + else io_load_error(path_apts, line, - _("syntax error in item repetition")); - } - if (is_recursive) { - recur_apoint_scan(data_file, start, end, - type, freq, until, notep, - &exc, state, filter); - } else { - apoint_scan(data_file, start, end, state, + _("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) { - if (is_recursive) { - recur_event_scan(data_file, start, id, - type, freq, until, notep, - &exc, filter); - } else { - event_scan(data_file, start, id, notep, - filter); - } + 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__); } @@ -928,16 +940,6 @@ load_keys_ht_compare(struct ht_keybindings_s *data1, } /* - * 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. @@ -946,21 +948,21 @@ static int is_blank(int c) */ void io_load_keys(const char *pager) { - struct ht_keybindings_s keys[NBKEYS]; + struct ht_keybindings_s virt_keys[NBVKEYS], *ht_elm, ht_entry; FILE *keyfp; - char buf[BUFSIZ]; + char buf[BUFSIZ], key_label[BUFSIZ], key_str[BUFSIZ]; + char *p, *msg; struct io_file *log; - int i, skipped, loaded, line; - const int MAX_ERRORS = 5; + int i, n, skipped, loaded, line, assigned, undefined, key; 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]); + 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"); @@ -969,111 +971,97 @@ void io_load_keys(const char *pager) 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') + p = buf; + while (*p == ' ' || *p == '\t') p++; + if (*p == '#' || *p == '\n') continue; - if (sscanf(buf, "%s", key_label) != AWAITED) { + /* 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; } - - /* Skip legacy entries. */ - if (strcmp(key_label, "generic-cut") == 0) - continue; - + p += strlen(key_label); ht_entry.label = key_label; - p = buf + strlen(key_label) + 1; - ht_elm = - HTABLE_LOOKUP(ht_keybindings, &ht_keys, &ht_entry); + ht_elm = HTABLE_LOOKUP(ht_keybindings, &ht_keys, &ht_entry); if (!ht_elm) { skipped++; - io_log_print(log, line, - _("Key label not recognized")); + asprintf(&msg, + _("Key label not recognized: \"%s\""), + key_label); + io_log_print(log, line, msg); + mem_free(msg); 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; + /* 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(&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++; - } + asprintf(&msg, + _("No keys assigned to " + "\"%s\"."), + key_label); + io_log_print(log, line, msg); + mem_free(msg); } - p += strlen(key_ch) + 1; - } else { - if (assigned) - loaded++; 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) { - const char *view_log = - _("There were some errors when loading keys file."); - io_log_display(log, view_log, pager); + 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); - 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!")); + /* 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) @@ -1195,21 +1183,6 @@ int io_check_data_files(void) return missing; } -/* Draw the startup screen */ -void io_startup_screen(int no_data_file) -{ - const char *enter = _("Press [ENTER] to continue"); - - if (no_data_file) - status_mesg(_("Welcome to Calcurse. Missing data files were created."), - enter); - else - status_mesg(_("Data files found. Data will be loaded now."), - enter); - - keys_wait_for_any_key(win[KEY].p); -} - /* Export calcurse data. */ void io_export_data(enum export_type type, int export_uid) { @@ -1240,7 +1213,7 @@ void io_export_data(enum export_type type, int export_uid) else if (type == IO_EXPORT_PCAL) pcal_export_data(stream); - if (show_dialogs() && ui_mode == UI_CURSES) { + if (!quiet && ui_mode == UI_CURSES) { fclose(stream); status_mesg(success, enter); keys_wait_for_any_key(win[KEY].p); @@ -1280,7 +1253,7 @@ static FILE *get_import_stream(enum import_type type, char **stream_name) * 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, +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) @@ -1315,7 +1288,7 @@ void io_import_data(enum import_type type, char *stream_name, } if (stream == NULL) - return; + return 0; memset(&stats, 0, sizeof stats); @@ -1323,7 +1296,7 @@ void io_import_data(enum import_type type, char *stream_name, if (log == NULL) { if (stream != stdin) file_close(stream, __FILE_POS__); - return; + return 0; } if (type == IO_IMPORT_ICAL) @@ -1348,7 +1321,7 @@ void io_import_data(enum import_type type, char *stream_name, stats.todos); asprintf(&stats_str[3], _("%d skipped"), stats.skipped); - if (ui_mode == UI_CURSES && show_dialogs()) { + if (ui_mode == UI_CURSES && !quiet) { char *read, *stat; asprintf(&read, proc_report, stats.lines); @@ -1359,7 +1332,7 @@ void io_import_data(enum import_type type, char *stream_name, mem_free(read); mem_free(stat); keys_wait_for_any_key(win[KEY].p); - } else if (ui_mode == UI_CMDLINE && show_dialogs()) { + } 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]); @@ -1380,8 +1353,11 @@ void io_import_data(enum import_type type, char *stream_name, mem_free(stats_str[3]); if (ui_mode == UI_CURSES) mem_free(stream_name); - if (!stats.skipped) + if (!stats.skipped) { io_log_free(log); + return 1; + } else + return 0; } struct io_file *io_log_init(void) @@ -1399,7 +1375,7 @@ struct io_file *io_log_init(void) ERROR_MSG(_("Warning: could not create temporary log file, Aborting...")); goto error; } - strncpy(log->name, logname, sizeof(log->name)); + 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...")); @@ -1449,6 +1425,7 @@ void io_log_free(struct io_file *log) EXIT_IF(unlink(log->name) != 0, _("Warning: could not erase temporary log file %s, Aborting..."), log->name); + mem_free(log->name); mem_free(log); } @@ -1457,7 +1434,8 @@ static void *io_psave_thread(void *arg) { int delay = conf.periodic_save; EXIT_IF(delay < 0, _("Invalid delay")); - char *mesg = _("Periodic save: data files have changed. Save cancelled."); + char *mesg = _("Periodic save cancelled. Data files have changed. " + "Save and merge interactively"); pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL); for (;;) { @@ -1577,22 +1555,6 @@ unsigned io_get_pid(char *file) } /* - * 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) @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -39,26 +39,73 @@ #include "calcurse.h" -#define MAXKEYVAL KEY_MAX /* ncurses defines KEY_MAX as maximum key value */ - -struct keydef_s { - const char *label; - const char *binding; - const char *sb_label; -}; +/* + * The interactive calcurse interface is controlled by "virtual keys", aka + * actions or commands. The virtual keys are defined by the type 'enum vkey', + * see calcurse.h. To each virtual key is assigned (or bound) zero or more + * keyboard keys/characters. A character (generated by a keyboard key) may be + * either an ordinary character or a pseudo-character. [An ordinary character + * is either a singlebyte ASCII character or a multibyte, UTF-8 encoded + * character; a pseudo-character (as supported by curses) is an escape sequence + * generated by a key.] A keyboard key/character is uniquely identified by its + * keyname (a character string) or by an integer (the Unicode code point of the + * character with a slight modification to accomodate the range of curses + * pseudo-characters). Mapping between the two forms is performed by the + * functions keys_str2int() and keys_int2str(). + */ -static llist_t keys[NBKEYS]; -static enum key actions[MAXKEYVAL]; +/* + * Assignment of keys to virtual keys is held in the tabel keys[]. The entry for + * each virtual key is a linked list of keyboard keys (bindings); each list + * element is the keyname as returned by keys_int2str(). + * + * At the very first run default keys are assigned to all virtual keys from a + * built-in table keydef[] and saved to disk in the calcurse config directory. + * Later the user may edit the key configuration and change the key bindings by + * adding/removing keys. If all keys are removed, the virtual key is left + * undefined. This state is also saved in the configuration file. The linked + * list for an undefined virtual key contains a single element with a null + * pointer as data. + */ +static llist_t keys[NBVKEYS]; +/* + * To cater for the other direction (which virtual key is a keyboard key + * assigned to), two constructions are needed: a table actions[] for the + * keyboard keys in the curses range, and a linked list actions_ext for + * multi-byte UTF-8 encoded keyboard characters. + * + * For each keyboard key (integer) in the curses key range, the virtual key + * (action) it is assigned to or, if not assigned, KEY_UNDEF. + */ +static enum vkey actions[KEY_MAX]; +/* + * For the millions of possible keyboard keys above the curses range, a linked + * list of keys which are actually bound to a virtual key. + * Each list element is a key_ext structure. + */ +llist_t actions_ext; struct key_ext { - int ch; - enum key action; + int key; + enum vkey action; }; -llist_t actions_ext; +/* + * Assigning a keyboard key to a virtual key is accomplished by + * 1) either inserting the virtual key in the actions[] entry for the keyboard key + * or adding the pair (key, virtual key) to the list actions_ext + * 2) adding it in keys[] to the list for the virtual key + * See keys_assign_binding() below. + */ +/* The default key bindings for the virtual keys. */ +struct keydef_s { + const char *label; /* Name of the virtual key (action). */ + const char *binding; /* String of space-separated keynames bound to it. */ + const char *sb_label; /* Display name in the status bar menu. */ +}; #define gettext_noop(s) s -static struct keydef_s keydef[NBKEYS] = { +static struct keydef_s keydef[NBVKEYS] = { { "generic-cancel", "ESC", gettext_noop("Cancel") }, { "generic-select", "SPC", gettext_noop("Select") }, { "generic-credits", "@", gettext_noop("Credits") }, @@ -69,6 +116,7 @@ static struct keydef_s keydef[NBKEYS] = { { "generic-copy", "c", gettext_noop("Copy") }, { "generic-paste", "p ^V", gettext_noop("Paste") }, { "generic-change-view", "TAB", gettext_noop("Chg Win") }, + { "generic-prev-view", "KEY_BTAB", gettext_noop("Prev Win") }, { "generic-import", "i I", gettext_noop("Import") }, { "generic-export", "x X", gettext_noop("Export") }, { "generic-goto", "g G", gettext_noop("Go to") }, @@ -132,10 +180,11 @@ void keys_init(void) int i; const char *cp; - for (i = 0; i < MAXKEYVAL; i++) + /* All keys unassigned. */ + for (i = 0; i < KEY_MAX; i++) actions[i] = KEY_UNDEF; LLIST_INIT(&actions_ext); - for (i = 0; i < NBKEYS; i++) + for (i = 0; i < NBVKEYS; i++) LLIST_INIT(&keys[i]); /* Initialization of the keynames table. */ @@ -146,7 +195,7 @@ void keys_init(void) for (i = 1; i < 128; i++) if ((cp = keyname(i))) keynames[i] = mem_strdup(cp); - /* ... and for the ncurses escape keys (pseudokeys). */ + /* ... and for the ncurses pseudo-characters. */ for (i = KEY_MIN; i < KEY_MAX; i++) if ((cp = keyname(i))) keynames[i] = mem_strdup(cp); @@ -189,7 +238,7 @@ void keys_free(void) { int i; - for (i = 0; i < NBKEYS; i++) { + for (i = 0; i < NBVKEYS; i++) { LLIST_FREE_INNER(&keys[i], key_free); LLIST_FREE(&keys[i]); } @@ -205,31 +254,40 @@ void keys_dump_defaults(char *file) _("FATAL ERROR: could not create default keys file.")); dump_intro(fd); - for (i = 0; i < NBKEYS; i++) + for (i = 0; i < NBVKEYS; i++) fprintf(fd, "%s %s\n", keydef[i].label, keydef[i].binding); file_close(fd, __FILE_POS__); } -const char *keys_get_label(enum key key) +const char *keys_get_label(enum vkey key) { EXIT_IF(key < 0 - || key > NBKEYS, + || key > NBVKEYS, _("FATAL ERROR: key value out of bounds")); return keydef[key].label; } +const char *keys_get_binding(enum vkey key) +{ + EXIT_IF(key < 0 + || key > NBVKEYS, + _("FATAL ERROR: key value out of bounds")); + + return keydef[key].binding; +} + static int key_ext_hasch(struct key_ext *k, void *cbdata) { - return (k->ch == *((int *)cbdata)); + return (k->key == *((int *)cbdata)); } -enum key keys_get_action(int pressed) +enum vkey keys_get_action(int pressed) { if (pressed < 0) { return -1; - } else if (pressed > MAXKEYVAL) { + } else if (pressed > KEY_MAX) { llist_item_t *i = LLIST_FIND_FIRST(&actions_ext, &pressed, key_ext_hasch); if (!i) @@ -274,7 +332,7 @@ void keys_wait_for_any_key(WINDOW *win) keys_wgetch(win); } -enum key keys_get(WINDOW *win, int *count, int *reg) +enum vkey keys_get(WINDOW *win, int *count, int *reg) { int ch = '0'; @@ -312,60 +370,89 @@ enum key keys_get(WINDOW *win, int *count, int *reg) } } -static void add_key_str(enum key action, int key) +static void add_if_undefined(enum vkey action) { - if (action > NBKEYS) - return; + /* If list is empty, mark action as UNDEFINED. */ + if (!keys[action].head) + LLIST_ADD(&keys[action], NULL); +} - LLIST_ADD(&keys[action], keys_int2str(key)); +static void del_if_undefined(enum vkey action) +{ + /* Action UNDEFINED? */ + if (!LLIST_GET_DATA(LLIST_FIRST(&keys[action]))) + LLIST_REMOVE(&keys[action], keys[action].head); } -int keys_assign_binding(int key, enum key action) +static void free_key_str(char *str) { - if (key < 0) - return 1; - if (key > KEY_MAX) { - llist_item_t *i = LLIST_FIND_FIRST(&actions_ext, &key, key_ext_hasch); - if (i) - return 1; - struct key_ext *k = mem_malloc(sizeof(struct key_ext)); - k->ch = key; - k->action = action; - LLIST_ADD(&actions_ext, k); - } else { - if (actions[key] != KEY_UNDEF) - return 1; - actions[key] = action; - } - add_key_str(action, key); - return 0; + mem_free(str); } -static void del_key_str(enum key action, int key) +static void add_key_str(enum vkey action, int key) +{ + if (action > NBVKEYS) + return; + + del_if_undefined(action); + LLIST_ADD(&keys[action], keys_int2str(key)); +} + +static void del_key_str(enum vkey action, int key) { llist_item_t *i; - char *oldstr = keys_int2str(key);; + char *oldstr = keys_int2str(key), *j; - if (action > NBKEYS) + if (action > NBVKEYS) return; LLIST_FOREACH(&keys[action], i) { - if (strcmp(LLIST_GET_DATA(i), oldstr) == 0) { + if (strcmp((j = LLIST_GET_DATA(i)), oldstr) == 0) { LLIST_REMOVE(&keys[action], i); + free_key_str(j); goto cleanup; } } cleanup: + add_if_undefined(action); mem_free(oldstr); } -void keys_remove_binding(int key, enum key action) +/* + * Assign keyboard key "key" to virtual key "action" by + * + * - marking keyboard key "key" as used for virtual key "actual" + * - adding "key" to the list of assigned keys for "action" in the tabel keys[] + * + * The former is done by either inserting "action" in the "key" entry of tabel + * actions[], or for keys above the curses range, inserting (key, action) in the + * list actions_ext. + */ +int keys_assign_binding(int key, enum vkey action) +{ + if (key > KEY_MAX) { + if (LLIST_FIND_FIRST(&actions_ext, &key, key_ext_hasch)) + return 1; + struct key_ext *k = mem_malloc(sizeof(struct key_ext)); + k->key = key; + k->action = action; + LLIST_ADD(&actions_ext, k); + } else if (key > -1) { + if (actions[key] != KEY_UNDEF) + return 1; + actions[key] = action; + } + add_key_str(action, key); + return 0; +} + +void keys_remove_binding(int key, enum vkey action) { if (key < 0) return; - if (key <= MAXKEYVAL) { + if (key <= KEY_MAX) { actions[key] = KEY_UNDEF; } else { llist_item_t *i = LLIST_FIND_FIRST(&actions_ext, &key, @@ -410,6 +497,8 @@ char *keys_int2str(int key) { char *res; + if (key == -1) + return NULL; if (key < KEY_MAX) { if (strcmp(keynames[key], "") == 0) return NULL; @@ -421,50 +510,44 @@ char *keys_int2str(int key) } } -int keys_action_count_keys(enum key action) +int keys_action_count_keys(enum vkey action) { llist_item_t *i; int n = 0; + /* Action UNDEFINED? */ + if (!LLIST_GET_DATA(LLIST_FIRST(&keys[action]))) + return 0; + LLIST_FOREACH(&keys[action], i) - n++; + n++; return n; } -const char *keys_action_firstkey(enum key action) +const char *keys_action_firstkey(enum vkey action) { const char *s = LLIST_GET_DATA(LLIST_FIRST(&keys[action])); return (s != NULL) ? s : "XXX"; } -const char *keys_action_nkey(enum key action, int keynum) +const char *keys_action_nkey(enum vkey action, int keynum) { return LLIST_GET_DATA(LLIST_NTH(&keys[action], keynum)); } -char *keys_action_allkeys(enum key action) +char *keys_action_allkeys(enum vkey action) { llist_item_t *i; - static char keystr[BUFSIZ]; - int keystrlen = 0; - int entrylen; - - if (!LLIST_FIRST(&keys[action])) - return NULL; - - keystr[0] = '\0'; - LLIST_FOREACH(&keys[action], i) { - entrylen = strlen(LLIST_GET_DATA(i)) + 1; - if (keystrlen + entrylen >= BUFSIZ) - break; - memcpy(keystr + keystrlen, LLIST_GET_DATA(i), entrylen - 1); - keystr[keystrlen + entrylen - 1] = ' '; - keystrlen += entrylen; - } - - keystr[keystrlen] = '\0'; - return keystr; + struct string keystr; + + string_init(&keystr); + if (!LLIST_GET_DATA(LLIST_FIRST(&keys[action]))) + string_catf(&keystr, "%s", "UNDEFINED"); + else + LLIST_FOREACH(&keys[action], i) + string_catf(&keystr, "%s ", LLIST_GET_DATA(i)); + return string_buf(&keystr); } /* Need this to display keys properly inside status bar. */ @@ -509,7 +592,7 @@ keys_display_bindings_bar(WINDOW * win, int *bindings, int count, const char *label; - if (binding_key < NBKEYS) { + if (binding_key < NBVKEYS) { strncpy(key, keys_action_firstkey(binding_key), UTF8_MAXLEN); key[UTF8_MAXLEN] = '\0'; label = gettext(keydef[binding_key].sb_label); @@ -561,9 +644,9 @@ keys_display_bindings_bar(WINDOW * win, int *bindings, int count, * Display information about the given key. * (could not add the keys descriptions to keydef variable, because of i18n). */ -void keys_popup_info(enum key key) +void keys_popup_info(enum vkey key) { - char *info[NBKEYS]; + char *info[NBVKEYS]; WINDOW *infowin; info[KEY_GENERIC_CANCEL] = _("Cancel the ongoing action."); @@ -582,6 +665,8 @@ void keys_popup_info(enum key key) _("Paste an item at the current position."); info[KEY_GENERIC_CHANGE_VIEW] = _("Select next panel in calcurse main screen."); + info[KEY_GENERIC_PREV_VIEW] = + _("Select previous panel in calcurse main screen."); info[KEY_GENERIC_IMPORT] = _("Import data from an external file."); info[KEY_GENERIC_EXPORT] = _("Export data to a new file format."); info[KEY_GENERIC_GOTO] = _("Select the day to go to."); @@ -651,7 +736,7 @@ void keys_popup_info(enum key key) info[KEY_LOWER_PRIORITY] = _("Lower a task priority inside the todo panel."); - if (key > NBKEYS) + if (key > NBVKEYS) return; #define WINROW 10 @@ -668,59 +753,75 @@ void keys_popup_info(enum key key) void keys_save_bindings(FILE * fd) { int i; - char *action; + char *keys; EXIT_IF(fd == NULL, _("FATAL ERROR: null file pointer.")); dump_intro(fd); - for (i = 0; i < NBKEYS; i++) { - action = keys_action_allkeys(i); - if (action) - fprintf(fd, "%s %s\n", keydef[i].label, action); + for (i = 0; i < NBVKEYS; i++) { + if ((keys = keys_action_allkeys(i))) + fprintf(fd, "%s %s\n", keydef[i].label, keys); } + mem_free(keys); } -int keys_check_missing_bindings(void) +int keys_check_undefined(void) { int i; - for (i = 0; i < NBKEYS; i++) { - if (!LLIST_FIRST(&keys[i])) + for (i = 0; i < NBVKEYS; i++) { + if (!LLIST_GET_DATA(LLIST_FIRST(&keys[i]))) return 1; } return 0; } -void keys_fill_missing(void) +int keys_check_missing(void) { int i; - for (i = 0; i < NBKEYS; i++) { - if (!LLIST_FIRST(&keys[i])) { - char *p, tmpbuf[BUFSIZ]; - - strncpy(tmpbuf, keydef[i].binding, BUFSIZ); - tmpbuf[BUFSIZ - 1] = '\0'; - p = tmpbuf; - for (;;) { - char key_ch[BUFSIZ]; - - while (*p == ' ') - p++; - if (sscanf(p, "%s", key_ch) == 1) { - int ch, used; - - ch = keys_str2int(key_ch); - used = keys_assign_binding(ch, i); - if (used) - WARN_MSG(_("When adding default key for \"%s\", " - "\"%s\" was already assigned!"), - keydef[i].label, - key_ch); - p += strlen(key_ch); - } else { - break; - } - } + for (i = 0; i < NBVKEYS; i++) { + if (!LLIST_FIRST(&keys[i])) + return 1; + } + return 0; +} + +/* + * Insert default keybindings for missing actions. + * Return either the number of actions assigned to (on success) or, if default + * keys could not be assigned, the negative index into the keydef[] table of the + * failing action. + */ +int keys_fill_missing(void) +{ + int i, ch, assign, assigned; + char *p, key_ch[BUFSIZ]; + + for (i = assigned = 0; i < NBVKEYS; i++) { + if (LLIST_FIRST(&keys[i])) + continue; + + p = (char *)keydef[i].binding; + for (assign = 0;;) { + while (*p == ' ') + p++; + if (sscanf(p, "%s", key_ch) == 1) { + ch = keys_str2int(key_ch); + if (keys_assign_binding(ch, i)) + return -i; + else + assign = 1; + p += strlen(key_ch); + } else + break; } + assigned += assign; + } + + if (assigned) { + p = (assigned == 1) ? "": "s"; + WARN_MSG(_("Default key(s) assigned to %d action%s."), + assigned, p); } + return assigned; } diff --git a/src/listbox.c b/src/listbox.c index 2b39aa8..d86f540 100644 --- a/src/listbox.c +++ b/src/listbox.c @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/llist.c b/src/llist.c index aca0a9d..d31f004 100644 --- a/src/llist.c +++ b/src/llist.c @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -109,6 +109,34 @@ llist_item_t *llist_next(llist_item_t * i) } /* + * Return the predecessor of a list item or, if head, the list item itself, + * or if not in the list, NULL. + * The list item may be supplied either directly (i) or as a pointer to + * the contents (data); the first case takes precedence. + */ +static llist_item_t *llist_prev(llist_t *l, llist_item_t *i, void *data) +{ + llist_item_t *j; + + if (!i && !data) + return NULL; + + if (l->head == i || l->head->data == data) + return l->head; + + if (i) { + for (j = l->head; j; j = j->next) + if (j->next == i) + return j; + } else { + for (j = l->head; j && j->next; j = j->next) + if (j->next->data == data) + return j; + } + return NULL; +} + +/* * Return the successor of a list item if it is matched by some filter * callback. Return NULL otherwise. */ @@ -150,34 +178,94 @@ void llist_add(llist_t * l, void *data) } /* + * Insert an existing item in a sorted list. + */ +static void llist_relink(llist_t *l, llist_item_t *i, llist_fn_cmp_t fn_cmp) +{ + llist_item_t *j; + + if (!i) + return; + + i->next = NULL; + if (!l->head) { + l->head = l->tail = i; + } else if (fn_cmp(i->data, l->tail->data) >= 0) { + l->tail->next = i; + l->tail = i; + } else if (fn_cmp(i->data, l->head->data) < 0) { + i->next = l->head; + l->head = i; + } else { + j = l->head; + while (j->next && fn_cmp(i->data, j->next->data) >= 0) + j = j->next; + i->next = j->next; + j->next = i; + } +} + +/* + * Unlink an item from a list and return it. + */ +static llist_item_t *llist_unlink(llist_t *l, llist_item_t *i) +{ + llist_item_t *p; + + if (!i) + return NULL; + + p = llist_prev(l, i, NULL); + if (!p) + return NULL; + + if (i == l->tail) + l->tail = (i == l->head) ? NULL : p; + if (i == l->head) + l->head = i->next; + else + p->next = i->next; + i->next = NULL; + return i; +} + +/* + * Find an item matched by some filter callback; start from a specified item. + */ +static llist_item_t *llist_find_from(llist_item_t *i, void *data, + llist_fn_match_t fn_match) +{ + if (!i) + return NULL; + + if (fn_match) { + for (; i; i = i->next) { + if (fn_match(i->data, data)) + return i; + } + } else { + for (; i; i = i->next) { + if (i->data == data) + return i; + } + } + + return NULL; +} + +/* * Add an item to a sorted list. */ void llist_add_sorted(llist_t * l, void *data, llist_fn_cmp_t fn_cmp) { llist_item_t *o = mem_malloc(sizeof(llist_item_t)); - llist_item_t *i; if (o) { o->data = data; o->next = NULL; - - if (!l->head) { - l->head = l->tail = o; - } else if (fn_cmp(o->data, l->tail->data) >= 0) { - l->tail->next = o; - l->tail = o; - } else if (fn_cmp(o->data, l->head->data) < 0) { - o->next = l->head; - l->head = o; - } else { - i = l->head; - while (i->next - && fn_cmp(o->data, i->next->data) >= 0) - i = i->next; - o->next = i->next; - i->next = o; - } } + + llist_relink(l, o, fn_cmp); } /* @@ -209,21 +297,7 @@ void llist_remove(llist_t * l, llist_item_t * i) llist_item_t *llist_find_first(llist_t * l, void *data, llist_fn_match_t fn_match) { - llist_item_t *i; - - if (fn_match) { - for (i = l->head; i; i = i->next) { - if (fn_match(i->data, data)) - return i; - } - } else { - for (i = l->head; i; i = i->next) { - if (i->data == data) - return i; - } - } - - return NULL; + return l ? llist_find_from(l->head, data, fn_match) : NULL; } /* @@ -232,22 +306,7 @@ llist_item_t *llist_find_first(llist_t * l, void *data, llist_item_t *llist_find_next(llist_item_t * i, void *data, llist_fn_match_t fn_match) { - if (i) { - i = i->next; - if (fn_match) { - for (; i; i = i->next) { - if (fn_match(i->data, data)) - return i; - } - } else { - for (; i; i = i->next) { - if (i->data == data) - return i; - } - } - } - - return NULL; + return i ? llist_find_from(i->next, data, fn_match) : NULL; } /* @@ -261,17 +320,42 @@ llist_item_t *llist_find_nth(llist_t * l, int n, void *data, if (n < 0) return NULL; - if (fn_match) { - for (i = l->head; i; i = i->next) { - if (fn_match(i->data, data) && (n-- == 0)) - return i; - } - } else { - for (i = l->head; i; i = i->next) { - if ((i->data == data) && (n-- == 0)) - return i; - } + for (i = l->head; i; i = i->next, n--) { + i = llist_find_from(i, data, fn_match); + if (!i || !n) + return i; } return NULL; } + +/* + * Reorder a sorted linked list when an item has changed. + */ +void llist_reorder(llist_t *l, void *data, llist_fn_cmp_t fn_cmp) +{ + llist_item_t *o, *p; + + if (!(p = llist_prev(l, NULL, data))) + return; + + /* List head? */ + if (p->data == data) + o = p; + else + o = p->next; + + /* Sorted already? + * Note: p is either the previous element or o itself. + */ + if (o->next && + fn_cmp(p->data, o->data) <= 0 && + fn_cmp(o->data, o->next->data) <= 0) + return; + if (!o->next && + fn_cmp(p->data, o->data) <= 0) + return; + + /* Link manipulation only. */ + llist_relink(l, llist_unlink(l, o), fn_cmp); +} diff --git a/src/llist.h b/src/llist.h index 9533819..0dd15bf 100644 --- a/src/llist.h +++ b/src/llist.h @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -99,8 +99,11 @@ void *llist_get_data(llist_item_t *); void llist_add(llist_t *, void *); void llist_add_sorted(llist_t *, void *, llist_fn_cmp_t); void llist_remove(llist_t *, llist_item_t *); +void llist_reorder(llist_t *, void *, llist_fn_cmp_t); #define LLIST_ADD(l, data) llist_add(l, data) #define LLIST_ADD_SORTED(l, data, fn_cmp) \ llist_add_sorted(l, data, (llist_fn_cmp_t)fn_cmp) #define LLIST_REMOVE(l, i) llist_remove(l, i) +#define LLIST_REORDER(l, data, fn_cmp) \ + llist_reorder(l, data, (llist_fn_cmp_t)fn_cmp) diff --git a/src/llist_ts.h b/src/llist_ts.h index 6597cde..a4b6184 100644 --- a/src/llist_ts.h +++ b/src/llist_ts.h @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -90,3 +90,5 @@ struct llist_ts { #define LLIST_TS_REMOVE(l_ts, i) llist_remove ((llist_t *)l_ts, i) #define LLIST_TS_ADD_SORTED(l_ts, data, fn_cmp) \ llist_add_sorted ((llist_t *)l_ts, data, (llist_fn_cmp_t)fn_cmp) +#define LLIST_TS_REORDER(l_ts, data, fn_cmp) \ + llist_reorder((llist_t *)l_ts, data, (llist_fn_cmp_t)fn_cmp) @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -59,13 +59,9 @@ HTABLE_PROTOTYPE(htp, note_gc_hash) char *generate_note(const char *str) { char *sha1 = mem_malloc(SHA1_DIGESTLEN * 2 + 1); - char *notepath, *s; + char *notepath; FILE *fp; - /* Temporary hack */ - asprintf(&s, "%s\n", str); - str = s; - sha1_digest(str, sha1); asprintf(¬epath, "%s%s", path_notes, sha1); fp = fopen(notepath, "w"); @@ -74,7 +70,6 @@ char *generate_note(const char *str) fputs(str, fp); file_close(fp, __FILE_POS__); - mem_free(s); mem_free(notepath); return sha1; } @@ -99,9 +94,7 @@ void edit_note(char **note, const char *editor) const char *arg[] = { editor, tmppath, NULL }; wins_launch_external(arg); - if (io_file_is_empty(tmppath) > 0) { - erase_note(note); - } else if ((fp = fopen(tmppath, "r"))) { + if ((fp = fopen(tmppath, "r"))) { sha1_stream(fp, sha1); fclose(fp); *note = sha1; @@ -160,6 +153,17 @@ void note_read(char *buffer, FILE * fp) buffer[MAX_NOTESIZ] = '\0'; } +/* Read the contents of a note file */ +void note_read_contents(char *buffer, size_t buffer_len, FILE * fp) +{ + size_t read_count = fread(buffer, 1, buffer_len, fp); + if (read_count != buffer_len) + buffer[read_count] = '\0'; + else + memcpy(&buffer[buffer_len - 4], "...\0", 4); +} + + static void note_gc_extract_key(struct note_gc_hash *data, const char **key, int *len) { diff --git a/src/notify.c b/src/notify.c index 3d19511..6eda361 100644 --- a/src/notify.c +++ b/src/notify.c @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -34,6 +34,7 @@ * */ +#include <sys/wait.h> #include <time.h> #include <stdlib.h> #include <string.h> @@ -136,9 +137,6 @@ void notify_init_vars(void) strncpy(nbar.cmd, cmd, BUFSIZ); nbar.cmd[BUFSIZ - 1] = '\0'; - if ((nbar.shell = getenv("SHELL")) == NULL) - nbar.shell = "/bin/sh"; - nbar.notify_all = 0; pthread_attr_init(&detached_thread_attr); @@ -215,26 +213,18 @@ void notify_reinit_bar(void) /* Launch user defined command as a notification. */ unsigned notify_launch_cmd(void) { - int pid; + char const *arg[2] = { nbar.cmd, NULL }; + int pid, pin, pout, perr; if (notify_app.state & APOINT_NOTIFIED) return 1; notify_app.state |= APOINT_NOTIFIED; - pid = fork(); - - if (pid < 0) { - ERROR_MSG(_("error while launching command: could not fork")); - return 0; - } else if (pid == 0) { - /* Child: launch user defined command */ - if (execlp(nbar.shell, nbar.shell, "-c", nbar.cmd, NULL) < - 0) { - ERROR_MSG(_("error while launching command")); - _exit(1); - } - _exit(0); + if ((pid = shell_exec(&pin, &pout, &perr, 1, *arg, arg))) { + close(pin); + close(pout); + close(perr); } return 1; @@ -358,6 +348,9 @@ static void *notify_main_thread(void *arg) pthread_mutex_unlock(¬ify.mutex); notify_update_bar(); psleep(thread_sleep); + /* Reap the user-defined notifications. */ + while (waitpid(0, NULL, WNOHANG) > 0) + ; elapse += thread_sleep; if (elapse >= check_app) { elapse = 0; @@ -505,8 +498,7 @@ void notify_check_repeated(struct recur_apoint *i) current_time = time(NULL); pthread_mutex_lock(¬ify_app.mutex); if (recur_item_find_occurrence - (i->start, i->dur, &i->exc, i->rpt->type, i->rpt->freq, - i->rpt->until, get_today(), &real_app_time)) { + (i->start, i->dur, i->rpt, &i->exc, get_today(), &real_app_time)) { if (!notify_app.got_app) { if (real_app_time - current_time <= DAYINSEC) update_notify = 1; @@ -547,12 +539,10 @@ int notify_same_recur_item(struct recur_apoint *i) time_t item_start; /* Tomorrow? */ - recur_item_find_occurrence(i->start, i->dur, &i->exc, i->rpt->type, - i->rpt->freq, i->rpt->until, + recur_item_find_occurrence(i->start, i->dur, i->rpt, &i->exc, NEXTDAY(get_today()), &item_start); /* Today? */ - recur_item_find_occurrence(i->start, i->dur, &i->exc, i->rpt->type, - i->rpt->freq, i->rpt->until, + recur_item_find_occurrence(i->start, i->dur, i->rpt, &i->exc, get_today(), &item_start); pthread_mutex_lock(¬ify_app.mutex); if (notify_app.got_app && item_start == notify_app.time) @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -56,24 +56,22 @@ typedef void (*cb_dump_t) (FILE *, long, long, char *); */ static void foreach_date_dump(const long date_end, struct rpt *rpt, llist_t * exc, - long item_first_date, long item_dur, char *item_mesg, + long item_start, long item_dur, char *item_mesg, cb_dump_t cb_dump, FILE * stream) { long date, item_time; struct tm lt; time_t t; - t = item_first_date; + t = item_start; localtime_r(&t, <); lt.tm_hour = lt.tm_min = lt.tm_sec = 0; lt.tm_isdst = -1; date = mktime(<); - item_time = item_first_date - date; + item_time = item_start - date; while (date <= date_end && date <= rpt->until) { - if (recur_item_inday - (item_first_date, item_dur, exc, rpt->type, rpt->freq, - rpt->until, date)) { + if (recur_item_inday(item_start, item_dur, rpt, exc, date)) { (*cb_dump) (stream, date + item_time, item_dur, item_mesg); } @@ -104,8 +102,8 @@ static void pcal_export_header(FILE * stream) { fputs("# calcurse pcal export\n", stream); fputs("\n# =======\n# options\n# =======\n", stream); - fprintf(stream, "opt -A -K -l -m -F %s\n", - ui_calendar_week_begins_on_monday()? "Monday" : "Sunday"); + fprintf(stream, "opt -A -K -l -m -F %s\n", get_wday_default_string( + ui_calendar_get_wday_start())); fputs("# Display week number (i.e. 1-52) on every Monday\n", stream); fprintf(stream, "all monday in all week %%w\n"); diff --git a/src/queue.c b/src/queue.c index a00ebad..41cb69b 100644 --- a/src/queue.c +++ b/src/queue.c @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/recur.c b/src/recur.c index 54c43ba..12f76b8 100644 --- a/src/recur.c +++ b/src/recur.c @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -46,12 +46,45 @@ llist_ts_t recur_alist_p; llist_t recur_elist; +static void free_int(int *i) +{ + mem_free(i); +} + +void recur_free_int_list(llist_t *ilist) +{ + LLIST_FREE_INNER(ilist, free_int); + LLIST_FREE(ilist); +} + +void recur_int_list_dup(llist_t *l, llist_t *ilist) +{ + llist_item_t *i; + int *o, *p; + + LLIST_INIT(l); + + if (ilist->head) { + LLIST_FOREACH(ilist, i) { + p = LLIST_GET_DATA(i); + o = mem_malloc(sizeof(int)); + *o = *p; + LLIST_ADD(l, o); + } + } +} + +static int int_cmp(int *list, int *i) +{ + return *list == *i; +} + static void free_exc(struct excp *exc) { mem_free(exc); } -static void free_exc_list(llist_t * exc) +void recur_free_exc_list(llist_t * exc) { LLIST_FREE_INNER(exc, free_exc); LLIST_FREE(exc); @@ -62,6 +95,11 @@ static int exc_cmp_day(struct excp *a, struct excp *b) return a->st < b->st ? -1 : (a->st == b->st ? 0 : 1); } +static int exc_inday(struct excp *exc, time_t *day_start) +{ + return (date_cmp_day(exc->st, *day_start) == 0); +} + static void recur_add_exc(llist_t * exc, time_t day) { struct excp *o = mem_malloc(sizeof(struct excp)); @@ -70,7 +108,7 @@ static void recur_add_exc(llist_t * exc, time_t day) LLIST_ADD_SORTED(exc, o, exc_cmp_day); } -static void exc_dup(llist_t * in, llist_t * exc) +void recur_exc_dup(llist_t * in, llist_t * exc) { llist_item_t *i; @@ -103,10 +141,10 @@ char *recur_exc2str(llist_t *exc) } /* - * Update the list of exceptions from a string of days. Any positive number of + * Update a list of exceptions from a string of days. Any positive number of * spaces are allowed before, between and after the days. */ -int recur_update_exc(llist_t *exc, char *days) +int recur_str2exc(llist_t *exc, char *days) { int updated = 0; char *d; @@ -130,11 +168,11 @@ int recur_update_exc(llist_t *exc, char *days) else break; } - free_exc_list(exc); - exc_dup(exc, &nexc); + recur_free_exc_list(exc); + recur_exc_dup(exc, &nexc); updated = 1; cleanup: - free_exc_list(&nexc); + recur_free_exc_list(&nexc); return updated; } @@ -149,11 +187,16 @@ struct recur_event *recur_event_dup(struct recur_event *in) rev->mesg = mem_strdup(in->mesg); rev->rpt = mem_malloc(sizeof(struct rpt)); + /* Note. The linked lists are NOT copied and no memory allocated. */ rev->rpt->type = in->rpt->type; rev->rpt->freq = in->rpt->freq; rev->rpt->until = in->rpt->until; + LLIST_INIT(&rev->rpt->bymonth); + LLIST_INIT(&rev->rpt->bywday); + LLIST_INIT(&rev->rpt->bymonthday); + LLIST_INIT(&rev->rpt->exc); - exc_dup(&rev->exc, &in->exc); + recur_exc_dup(&rev->exc, &in->exc); if (in->note) rev->note = mem_strdup(in->note); @@ -176,11 +219,16 @@ struct recur_apoint *recur_apoint_dup(struct recur_apoint *in) rapt->mesg = mem_strdup(in->mesg); rapt->rpt = mem_malloc(sizeof(struct rpt)); + /* Note. The linked lists are NOT copied and no memory allocated. */ rapt->rpt->type = in->rpt->type; rapt->rpt->freq = in->rpt->freq; rapt->rpt->until = in->rpt->until; + LLIST_INIT(&rapt->rpt->bymonth); + LLIST_INIT(&rapt->rpt->bywday); + LLIST_INIT(&rapt->rpt->bymonthday); + LLIST_INIT(&rapt->rpt->exc); - exc_dup(&rapt->exc, &in->exc); + recur_exc_dup(&rapt->exc, &in->exc); if (in->note) rapt->note = mem_strdup(in->note); @@ -207,7 +255,7 @@ void recur_apoint_free(struct recur_apoint *rapt) mem_free(rapt->note); if (rapt->rpt) mem_free(rapt->rpt); - free_exc_list(&rapt->exc); + recur_free_exc_list(&rapt->exc); mem_free(rapt); } @@ -218,7 +266,7 @@ void recur_event_free(struct recur_event *rev) mem_free(rev->note); if (rev->rpt) mem_free(rev->rpt); - free_exc_list(&rev->exc); + recur_free_exc_list(&rev->exc); mem_free(rev); } @@ -261,28 +309,31 @@ static int recur_event_cmp(struct recur_event *a, struct recur_event *b) /* Insert a new recursive appointment in the general linked list */ struct recur_apoint *recur_apoint_new(char *mesg, char *note, time_t start, - long dur, char state, int type, - int freq, time_t until, - llist_t * except) + long dur, char state, struct rpt *rpt) { struct recur_apoint *rapt = mem_malloc(sizeof(struct recur_apoint)); - rapt->rpt = mem_malloc(sizeof(struct rpt)); rapt->mesg = mem_strdup(mesg); rapt->note = (note != NULL) ? mem_strdup(note) : 0; rapt->start = start; - rapt->state = state; rapt->dur = dur; - rapt->rpt->type = type; - rapt->rpt->freq = freq; - rapt->rpt->until = until; - if (except) { - exc_dup(&rapt->exc, except); - free_exc_list(except); - } else { - LLIST_INIT(&rapt->exc); - } + rapt->state = state; + rapt->rpt = mem_malloc(sizeof(struct rpt)); + *rapt->rpt = *rpt; + recur_int_list_dup(&rapt->rpt->bymonth, &rpt->bymonth); + recur_free_int_list(&rpt->bymonth); + recur_int_list_dup(&rapt->rpt->bywday, &rpt->bywday); + recur_free_int_list(&rpt->bywday); + recur_int_list_dup(&rapt->rpt->bymonthday, &rpt->bymonthday); + recur_free_int_list(&rpt->bymonthday); + /* + * Note. The exception dates are in the list rapt->exc. + * The (empty) list rapt->rpt->exc is not used. + */ + recur_exc_dup(&rapt->exc, &rpt->exc); + recur_free_exc_list(&rpt->exc); + LLIST_INIT(&rapt->rpt->exc); LLIST_TS_LOCK(&recur_alist_p); LLIST_TS_ADD_SORTED(&recur_alist_p, rapt, recur_apoint_cmp); @@ -293,25 +344,26 @@ struct recur_apoint *recur_apoint_new(char *mesg, char *note, time_t start, /* Insert a new recursive event in the general linked list */ struct recur_event *recur_event_new(char *mesg, char *note, time_t day, - int id, int type, int freq, time_t until, - llist_t * except) + int id, struct rpt *rpt) { struct recur_event *rev = mem_malloc(sizeof(struct recur_event)); - rev->rpt = mem_malloc(sizeof(struct rpt)); rev->mesg = mem_strdup(mesg); rev->note = (note != NULL) ? mem_strdup(note) : 0; rev->day = day; rev->id = id; - rev->rpt->type = type; - rev->rpt->freq = freq; - rev->rpt->until = until; - if (except) { - exc_dup(&rev->exc, except); - free_exc_list(except); - } else { - LLIST_INIT(&rev->exc); - } + rev->rpt = mem_malloc(sizeof(struct rpt)); + *rev->rpt = *rpt; + recur_int_list_dup(&rev->rpt->bymonth, &rpt->bymonth); + recur_free_int_list(&rpt->bymonth); + recur_int_list_dup(&rev->rpt->bywday, &rpt->bywday); + recur_free_int_list(&rpt->bywday); + recur_int_list_dup(&rev->rpt->bymonthday, &rpt->bymonthday); + recur_free_int_list(&rpt->bymonthday); + /* Similarly as for recurrent appointment. */ + recur_exc_dup(&rev->exc, &rpt->exc); + recur_free_exc_list(&rpt->exc); + LLIST_INIT(&rev->rpt->exc); LLIST_ADD_SORTED(&recur_elist, rev, recur_event_cmp); @@ -340,8 +392,7 @@ char recur_def2char(enum recur_type define) recur_char = 'Y'; break; default: - EXIT(_("unknown repetition type")); - return 0; + recur_char = 0; } return recur_char; @@ -375,6 +426,39 @@ int recur_char2def(char type) return recur_def; } +/* Write the bymonthday list. */ +static void bymonthday_append(struct string *s, llist_t *l) +{ + llist_item_t *i; + + LLIST_FOREACH(l, i) { + int *day = LLIST_GET_DATA(i); + string_catf(s, " d%d", *day); + } +} + +/* Write the bywday list. */ +static void bywday_append(struct string *s, llist_t *l) +{ + llist_item_t *i; + + LLIST_FOREACH(l, i) { + int *wday = LLIST_GET_DATA(i); + string_catf(s, " w%d", *wday); + } +} + +/* Write the bymonth list. */ +static void bymonth_append(struct string *s, llist_t *l) +{ + llist_item_t *i; + + LLIST_FOREACH(l, i) { + int *mon = LLIST_GET_DATA(i); + string_catf(s, " m%d", *mon); + } +} + /* Write days for which recurrent items should not be repeated. */ static void recur_exc_append(struct string *s, llist_t *lexc) { @@ -395,29 +479,25 @@ static void recur_exc_append(struct string *s, llist_t *lexc) } /* Load the recursive appointment description */ -struct recur_apoint *recur_apoint_scan(FILE * f, struct tm start, - struct tm end, char type, int freq, - struct tm until, char *note, - llist_t * exc, char state, - struct item_filter *filter) +char *recur_apoint_scan(FILE *f, struct tm start, struct tm end, + char state, char *note, + struct item_filter *filter, + struct rpt *rpt) { char buf[BUFSIZ], *nl; - time_t tstart, tend, tuntil; + time_t tstart, tend; struct recur_apoint *rapt = NULL; int cond; - EXIT_IF(!check_date(start.tm_year, start.tm_mon, start.tm_mday) || - !check_date(end.tm_year, end.tm_mon, end.tm_mday) || - !check_time(start.tm_hour, start.tm_min) || - !check_time(end.tm_hour, end.tm_min) || - (until.tm_year != 0 - && !check_date(until.tm_year, until.tm_mon, - until.tm_mday)), - _("date error in appointment")); + if (!check_date(start.tm_year, start.tm_mon, start.tm_mday) || + !check_date(end.tm_year, end.tm_mon, end.tm_mday) || + !check_time(start.tm_hour, start.tm_min) || + !check_time(end.tm_hour, end.tm_min)) + return _("illegal date in appointment"); /* Read the appointment description */ if (!fgets(buf, sizeof buf, f)) - return NULL; + return _("error in appointment description"); nl = strchr(buf, '\n'); if (nl) { @@ -432,19 +512,15 @@ struct recur_apoint *recur_apoint_scan(FILE * f, struct tm start, tstart = mktime(&start); tend = mktime(&end); - if (until.tm_year != 0) { - until.tm_hour = 0; - until.tm_min = 0; - until.tm_sec = 0; - until.tm_isdst = -1; - until.tm_year -= 1900; - until.tm_mon--; - tuntil = mktime(&until); - } else { - tuntil = 0; + if (tstart == -1 || tend == -1 || tstart > tend) + return _("date error in appointment"); + + /* Does it occur on the start day? */ + if (!recur_item_find_occurrence(tstart, tend - tstart, rpt, NULL, + DAY(tstart), NULL)) { + char *fmt = _("recurrence error: not on start day (%s)"); + return day_ins(&fmt, tstart); } - EXIT_IF(tstart == -1 || tend == -1 || tstart > tend - || tuntil == -1, _("date error in appointment")); /* Filter item. */ if (filter) { @@ -458,9 +534,8 @@ struct recur_apoint *recur_apoint_scan(FILE * f, struct tm start, ); if (filter->hash) { rapt = recur_apoint_new(buf, note, tstart, - tend - tstart, state, - recur_char2def(type), - freq, tuntil, exc); + tend - tstart, state, + rpt); char *hash = recur_apoint_hash(rapt); cond = cond || !hash_matches(filter->hash, hash); mem_free(hash); @@ -473,54 +548,51 @@ struct recur_apoint *recur_apoint_scan(FILE * f, struct tm start, } } if (!rapt) - rapt = recur_apoint_new(buf, note, tstart, tend - tstart, - state, recur_char2def(type), freq, - tuntil, exc); - - return rapt; + rapt = recur_apoint_new(buf, note, tstart, tend - tstart, state, + rpt); + return NULL; } /* Load the recursive events from file */ -struct recur_event *recur_event_scan(FILE * f, struct tm start, int id, - char type, int freq, struct tm until, - char *note, llist_t * exc, - struct item_filter *filter) +char *recur_event_scan(FILE * f, struct tm start, int id, + char *note, struct item_filter *filter, + struct rpt *rpt) { char buf[BUFSIZ], *nl; - time_t tstart, tend, tuntil; + time_t tstart, tend; struct recur_event *rev = NULL; int cond; - EXIT_IF(!check_date(start.tm_year, start.tm_mon, start.tm_mday) || - !check_time(start.tm_hour, start.tm_min) || - (until.tm_year != 0 - && !check_date(until.tm_year, until.tm_mon, - until.tm_mday)), _("date error in event")); + if (!check_date(start.tm_year, start.tm_mon, start.tm_mday) || + !check_time(start.tm_hour, start.tm_min)) + return _("illegel date in event"); /* Read the event description */ if (!fgets(buf, sizeof buf, f)) - return NULL; + return _("error in appointment description"); nl = strchr(buf, '\n'); if (nl) { *nl = '\0'; } - start.tm_hour = until.tm_hour = 0; - start.tm_min = until.tm_min = 0; - start.tm_sec = until.tm_sec = 0; - start.tm_isdst = until.tm_isdst = -1; + start.tm_hour = 0; + start.tm_min = 0; + start.tm_sec = 0; + start.tm_isdst = -1; start.tm_year -= 1900; start.tm_mon--; - if (until.tm_year != 0) { - until.tm_year -= 1900; - until.tm_mon--; - tuntil = mktime(&until); - } else { - tuntil = 0; - } + tstart = mktime(&start); - EXIT_IF(tstart == -1 || tuntil == -1, _("date error in event")); - tend = tstart + DAYINSEC - 1; + if (tstart == -1) + return _("date error in event"); + tend = ENDOFDAY(tstart); + + /* Does it occur on the start day? */ + if (!recur_item_find_occurrence(tstart, -1, rpt, NULL, + DAY(tstart), NULL)) { + char *fmt = _("recurrence error: not on start day (%s)"); + return day_ins(&fmt, tstart); + } /* Filter item. */ if (filter) { @@ -534,8 +606,7 @@ struct recur_event *recur_event_scan(FILE * f, struct tm start, int id, ); if (filter->hash) { rev = recur_event_new(buf, note, tstart, id, - recur_char2def(type), - freq, tuntil, exc); + rpt); char *hash = recur_event_hash(rev); cond = cond || !hash_matches(filter->hash, hash); mem_free(hash); @@ -548,11 +619,8 @@ struct recur_event *recur_event_scan(FILE * f, struct tm start, int id, } } if (!rev) - rev = recur_event_new(buf, note, tstart, id, - recur_char2def(type), - freq, tuntil, exc); - - return rev; + rev = recur_event_new(buf, note, tstart, id, rpt); + return NULL; } char *recur_apoint_tostr(struct recur_apoint *o) @@ -584,6 +652,9 @@ char *recur_apoint_tostr(struct recur_apoint *o) recur_def2char(o->rpt->type), lt.tm_mon + 1, lt.tm_mday, 1900 + lt.tm_year); } + bymonthday_append(&s, &o->rpt->bymonthday); + bywday_append(&s, &o->rpt->bywday); + bymonth_append(&s, &o->rpt->bymonth); recur_exc_append(&s, &o->exc); string_catf(&s, "} "); if (o->note) @@ -645,6 +716,9 @@ char *recur_event_tostr(struct recur_event *o) recur_def2char(o->rpt->type), end_mon, end_day, end_year); } + bymonthday_append(&s, &o->rpt->bymonthday); + bywday_append(&s, &o->rpt->bywday); + bymonth_append(&s, &o->rpt->bymonth); recur_exc_append(&s, &o->exc); string_catf(&s, "} "); if (o->note) @@ -690,6 +764,20 @@ void recur_save_data(FILE * f) } /* + * Return the month day counted from the opposite end of the month. + */ +static int opp_mday(int year, int month, int day) +{ + EXIT_IF(day == 0, _("month day is zero")); + + int m_days = days[month - 1] + (month == 2 && ISLEAP(year)); + if (day > 0) + return day - 1 - m_days; + else + return day + 1 + m_days; +} + +/* * The two following defines together with the diff_days, diff_months and * diff_years functions were provided by Lukas Fleischer to correct the wrong * calculation of recurrent dates after a turn of year. @@ -740,169 +828,739 @@ static long diff_years(struct tm lt_start, struct tm lt_end) return lt_end.tm_year - lt_start.tm_year; } -static int exc_inday(struct excp *exc, time_t *day_start) +/* + * Return true if 'mon' and 'mday' is month and day of t + * (after a call of mktime()). + */ +static int date_chk(time_t t, int mon, int mday) { - return (date_cmp_day(exc->st, *day_start) == 0); + struct tm tm; + + localtime_r(&t, &tm); + + return tm.tm_mon == mon && tm.tm_mday == mday; } /* - * Check if the recurrent item belongs to the selected day, and if yes, store - * the start date of the occurrence that belongs to the day in a buffer. - * - * This function was improved thanks to Tony's patch. - * Thanks also to youshe for reporting daylight saving time related problems. - * And finally thanks to Lukas for providing a patch to correct the wrong - * calculation of recurrent dates after a turn of years. + * Return true if the rrule (start, dur, rpt, exc) has an occurrence on the + * given day. If so, save that occurrence in a (dynamic or static) buffer. */ -unsigned -recur_item_find_occurrence(time_t item_start, long item_dur, - llist_t * item_exc, int rpt_type, int rpt_freq, - time_t rpt_until, time_t day_start, - time_t *occurrence) +static int find_occurrence(time_t start, long dur, struct rpt *rpt, llist_t *exc, + time_t day, time_t *occurrence) { -/* - * Function-internal duration - * 1) To avoid an item ending on midnight (which belongs to the next day), - * duration is always diminished by 1 second. - * 2) An event has no explicit duration, but lasts for an entire day, which - * in turn depends on DST. - */ -#define ITEM_DUR(d) ((item_dur == -1 ? DAYLEN(d) : item_dur) - 1) + /* + * Duration-on-day-d fix. + * An item cannot end on midnight or else it is counted towards the next day. + * An event (dur == -1) has no explicit duration, but is considered to last for + * the entire day (d) which depends on DST. + */ +#define DUR(d) (dur == -1 ? DAYLEN((d)) - 1 : dur - 1) long diff; - struct tm lt_day, lt_item, lt_item_day; - time_t occ, item_day_start; - - item_day_start = update_time_in_date(item_start, 0, 0); + struct tm lt_day, lt_start, lt_occur; + time_t t; + int mday, order, pwday, nwday, mon; - if (day_start < item_day_start) + /* Is the given day before the day of the first occurence? */ + if (date_cmp_day(day, start) < 0) return 0; - if (rpt_until && day_start >= - rpt_until + (item_start - item_day_start) + ITEM_DUR(rpt_until)) + /* + * - or after the day of the last occurrence (which may stretch beyond + * the until date)? Extraneous days are eliminated later. + */ + if (rpt->until && + date_cmp_day(NEXTDAY(rpt->until) + DUR(rpt->until), day) < 0) return 0; - localtime_r(&day_start, <_day); /* selected day */ - localtime_r(&item_start, <_item); /* first occurrence */ - lt_item_day = lt_item; /* recent occurrence */ + localtime_r(&day, <_day); /* Given day. */ + localtime_r(&start, <_start); /* Original item. */ + lt_occur = lt_start; /* First occurence. */ /* * Update to the most recent occurrence before or on the selected day. */ - switch (rpt_type) { + switch (rpt->type) { case RECUR_DAILY: - diff = diff_days(lt_item_day, lt_day) % rpt_freq; - lt_item_day.tm_mday = lt_day.tm_mday - diff; - lt_item_day.tm_mon = lt_day.tm_mon; - lt_item_day.tm_year = lt_day.tm_year; + /* Number of days since the most recent occurrence. */ + diff = diff_days(lt_occur, lt_day) % rpt->freq; + lt_occur.tm_mday = lt_day.tm_mday - diff; + lt_occur.tm_mon = lt_day.tm_mon; + lt_occur.tm_year = lt_day.tm_year; break; case RECUR_WEEKLY: - diff = diff_days(lt_item_day, lt_day) % - (rpt_freq * WEEKINDAYS); - lt_item_day.tm_mday = lt_day.tm_mday - diff; - lt_item_day.tm_mon = lt_day.tm_mon; - lt_item_day.tm_year = lt_day.tm_year; + diff = diff_days(lt_occur, lt_day) % + (rpt->freq * WEEKINDAYS); + lt_occur.tm_mday = lt_day.tm_mday - diff; + lt_occur.tm_mon = lt_day.tm_mon; + lt_occur.tm_year = lt_day.tm_year; break; case RECUR_MONTHLY: - diff = diff_months(lt_item_day, lt_day) % rpt_freq; - if (!diff && lt_day.tm_mday < lt_item_day.tm_mday) - diff += rpt_freq; - lt_item_day.tm_mon = lt_day.tm_mon - diff; - lt_item_day.tm_year = lt_day.tm_year; + diff = diff_months(lt_occur, lt_day) % rpt->freq; + if (!diff && lt_day.tm_mday < lt_occur.tm_mday) + diff += rpt->freq; + lt_occur.tm_mon = lt_day.tm_mon - diff; + lt_occur.tm_year = lt_day.tm_year; break; case RECUR_YEARLY: - diff = diff_years(lt_item_day, lt_day) % rpt_freq; + diff = diff_years(lt_occur, lt_day) % rpt->freq; if (!diff && - (lt_day.tm_mon < lt_item_day.tm_mon || - (lt_day.tm_mon == lt_item_day.tm_mon && - lt_day.tm_mday < lt_item_day.tm_mday))) - diff += rpt_freq; - lt_item_day.tm_year = lt_day.tm_year - diff; + (lt_day.tm_mon < lt_occur.tm_mon || + (lt_day.tm_mon == lt_occur.tm_mon && + lt_day.tm_mday < lt_occur.tm_mday))) + diff += rpt->freq; + lt_occur.tm_year = lt_day.tm_year - diff; break; default: EXIT(_("unknown item type")); } /* Switch to calendar (Unix) time. */ - lt_item_day.tm_isdst = -1; - occ = mktime(<_item_day); + lt_occur.tm_isdst = -1; + t = mktime(<_occur); /* * Impossible dates must be ignored (according to RFC 5545). Changing * only the year or the month may lead to dates like 29 February in * non-leap years or 31 November. */ - if (rpt_type == RECUR_MONTHLY || rpt_type == RECUR_YEARLY) { - localtime_r(&occ, <_item_day); - if (lt_item_day.tm_mday != lt_item.tm_mday) + if ((rpt->type == RECUR_MONTHLY || rpt->type == RECUR_YEARLY) && + !date_chk(t, lt_occur.tm_mon, lt_start.tm_mday)) + return 0; + + /* + * BYMONTHDAY reduction + * A month day has two possible list forms. + */ + mday = opp_mday(lt_occur.tm_year + 1900, lt_occur.tm_mon + 1, + lt_occur.tm_mday); + if (rpt->bymonthday.head && + rpt->type == RECUR_DAILY && + !LLIST_FIND_FIRST(&rpt->bymonthday, <_occur.tm_mday, int_cmp) && + !LLIST_FIND_FIRST(&rpt->bymonthday, &mday, int_cmp)) + return 0; + + /* BYDAY reduction for DAILY */ + if (rpt->bywday.head && rpt->type == RECUR_DAILY && + !LLIST_FIND_FIRST(&rpt->bywday, <_occur.tm_wday, int_cmp)) + return 0; + + /* + * BYDAY reduction for MONTHLY + * A weekday has three possible list forms. + */ + if (rpt->bywday.head && + rpt->type == RECUR_MONTHLY && rpt->bymonthday.head) { + /* positive order */ + order = (lt_occur.tm_mday + 6) / WEEKINDAYS; + pwday = order * WEEKINDAYS + lt_occur.tm_wday; + /* negative order */ + order = order + - wday_per_month(lt_occur.tm_mon + 1, + lt_occur.tm_year + 1900, + lt_occur.tm_wday) + - 1; + nwday = order * WEEKINDAYS - lt_occur.tm_wday; + if (!LLIST_FIND_FIRST(&rpt->bywday, <_occur.tm_wday, int_cmp) && + !LLIST_FIND_FIRST(&rpt->bywday, &pwday, int_cmp) && + !LLIST_FIND_FIRST(&rpt->bywday, &nwday, int_cmp)) + return 0; + } + + /* + * BYDAY reduction for YEARLY + * A weekday has three possible list forms. + */ + if (rpt->bywday.head && + rpt->type == RECUR_YEARLY && rpt->bymonthday.head) { + /* positive order */ + order = lt_occur.tm_yday / WEEKINDAYS; + pwday = order * WEEKINDAYS + lt_occur.tm_wday; + /* negative order */ + order = order + - wday_per_year(lt_occur.tm_year + 1900, + lt_occur.tm_wday) + - 1; + nwday = order * WEEKINDAYS - lt_occur.tm_wday; + if (!LLIST_FIND_FIRST(&rpt->bywday, <_occur.tm_wday, int_cmp) && + !LLIST_FIND_FIRST(&rpt->bywday, &pwday, int_cmp) && + !LLIST_FIND_FIRST(&rpt->bywday, &nwday, int_cmp)) return 0; } + /* BYMONTH reduction */ + mon = lt_occur.tm_mon + 1; + if (rpt->bymonth.head && + rpt->type != RECUR_YEARLY && + !LLIST_FIND_FIRST(&rpt->bymonth, &mon, int_cmp)) + return 0; + /* Exception day? */ - if (LLIST_FIND_FIRST(item_exc, &occ, exc_inday)) + if (exc && LLIST_FIND_FIRST(exc, &t, exc_inday)) return 0; - /* After until day? */ - if (rpt_until && occ >= NEXTDAY(rpt_until)) + /* Extraneous day? */ + if (rpt->until && t >= NEXTDAY(rpt->until)) return 0; - /* Does it span the selected day? */ - if (occ + ITEM_DUR(occ) < day_start) + /* Does it span the given day? + * + * NOTE: An appointment ending at 00:00 is not considered to span the + * given day, unless the appointment is an appointment without + * specified end time, which is internally treated as appointment with + * duration 0. + */ + if (t + DUR(t) >= day || (t == day && dur == 0)) { + if (occurrence) + *occurrence = t; + return 1; + } else { return 0; + } +} +#undef DUR - if (occurrence) - *occurrence = occ; +/* + * Return true if the rrule (s, d, r, e) has an occurrence, depending + * on the frequency, in the year, month or week of day. + */ +static int freq_chk(time_t day, time_t s, long d, struct rpt *r, llist_t *e) +{ + if (r->type == RECUR_DAILY) + EXIT(_("no daily frequency check")); + + struct tm tm_start, tm_day; + struct rpt fc_rpt; + time_t fc_day, fc_s; + + localtime_r(&s, &tm_start); + localtime_r(&day, &tm_day); + + if (r->type == RECUR_WEEKLY) { + /* Set day to the weekly occurrence. */ + fc_day = date_sec_change( + day, + 0, + WDAY(tm_start.tm_wday) - WDAY(tm_day.tm_wday) + ); + fc_s = s; + } else { + /* The start day may be invalid in some months. */ + tm_day.tm_mday = tm_start.tm_mday = 1; + if (r->type == RECUR_YEARLY) + tm_day.tm_mon = tm_start.tm_mon; + tm_day.tm_isdst = tm_start.tm_isdst = -1; + fc_day = mktime(&tm_day); + fc_s = mktime(&tm_start); + } + /* Turn all reductions off. */ + fc_rpt = *r; + fc_rpt.until = 0; + fc_rpt.bymonth.head = fc_rpt.bywday.head = fc_rpt.bymonthday.head = NULL; - return 1; -#undef ITEM_DUR + return find_occurrence(fc_s, d, &fc_rpt, e, fc_day, NULL); } +/* + * Return true if the rrule (s, d, r, e) has an occurrence on 'day' after + * 'first'; if so, return it in occurrence. + */ +static int test_occurrence(time_t s, long d, struct rpt *r, llist_t *e, + time_t first, time_t day, time_t *occurrence) +{ + time_t occ; + + if (find_occurrence(s, d, r, e, day, &occ)) { + if (occ < first) + return 0; + if (occurrence) + *occurrence = occ; + return 1; + } + return 0; +} + +#define NO_EXPANSION -1 +static int expand_weekly(time_t start, long dur, struct rpt *rpt, llist_t *exc, + time_t day, time_t *occurrence) +{ + struct tm tm_start; + llist_item_t *i; + int *w; + time_t w_start; + + localtime_r(&start, &tm_start); + + /* BYDAY expansion */ + if (rpt->bywday.head) { + LLIST_FOREACH(&rpt->bywday, i) { + w = LLIST_GET_DATA(i); + if (*w < 0 || *w > 6) + continue; + /* + * Modify rrule start with a new day in the same week as + * start - taking first day of the week into account. + */ + w_start = date_sec_change( + start, + 0, + WDAY(*w) - WDAY(tm_start.tm_wday) + ); + if (test_occurrence(w_start, dur, rpt, exc, + start, day, occurrence)) + return 1; + } + } else + return NO_EXPANSION; + + /* No occurrence */ + return 0; +} + +static int expand_monthly(time_t start, long dur, struct rpt *rpt, llist_t *exc, + time_t day, time_t *occurrence) +{ + struct tm tm_start, tm_day; + llist_item_t *i; + int *w, mday, mon, valid; + time_t nstart; + struct rpt r = *rpt; + + localtime_r(&day, &tm_day); + + /* + * The following three conditional alternatives are mutually exclusive + * and cover all four cases of two booleans. + */ + + /* BYMONTHDAY expansion */ + if (rpt->bymonthday.head) { + LLIST_FOREACH(&rpt->bymonthday, i) { + mday = *(int *)LLIST_GET_DATA(i); + + if (mday < 0) + mday = opp_mday(tm_day.tm_year + 1900, + tm_day.tm_mon + 1, mday); + /* + * Modify rrule start with a new monthday. + * If it is invalid (29, 30 or 31) in the start month, + * the month is changed to an earlier one matching the + * frequency. + */ + localtime_r(&start, &tm_start); + mon = tm_start.tm_mon; + + tm_start.tm_mday = mday; + tm_start.tm_isdst = -1; + nstart = mktime(&tm_start); + valid = date_chk(nstart, mon, mday); + /* Never valid? */ + if (!valid && !(rpt->freq % 12)) + return 0; + /* Note. The loop will terminate! */ + while (!valid) { + localtime_r(&start, &tm_start); + mon -= rpt->freq; + tm_start.tm_mon = mon; + tm_start.tm_mday = mday; + tm_start.tm_isdst = -1; + nstart = mktime(&tm_start); + valid = date_chk(nstart, (mon + 12) % 12, mday); + } + if (test_occurrence(nstart, dur, rpt, exc, + start, day, occurrence)) + return 1; + } + } + /* BYDAY special expansion for MONTHLY */ + else if (rpt->bywday.head) { + /* The frequency is modified later. */ + if (!freq_chk(day, start, dur, rpt, exc)) + return 0; + + LLIST_FOREACH(&rpt->bywday, i) { + w = LLIST_GET_DATA(i); + + int order, wday, nbwd; + + localtime_r(&start, &tm_start); + /* + * Construct a weekly rrule; BYMONTH-reduction in + * find_occurrence() will reduce to the bymonth list. + */ + r.type = RECUR_WEEKLY; + if (*w > 6) { + /* + * A single occurrence counting forwards from + * the start of the month. + */ + order = *w / WEEKINDAYS; + wday = *w % WEEKINDAYS; + nbwd = wday_per_month(tm_day.tm_mon + 1, + tm_day.tm_year + 1900, + wday); + if (nbwd < order) + return 0; + r.freq = order; + tm_start.tm_mday = 1; + tm_start.tm_mon = tm_day.tm_mon; + tm_start.tm_year = tm_day.tm_year; + tm_start.tm_isdst = -1; + /* Start in the week before the month. */ + nstart = date_sec_change( + next_wday(mktime(&tm_start), wday), + 0, + -WEEKINDAYS + ); + r.until = date_sec_change( + DAY(nstart), + 0, + r.freq * WEEKINDAYS + ); + if (rpt->until && r.until > rpt->until) + return 0; + } else if (*w > -1) { + /* Expansion to each week. */ + wday = *w % WEEKINDAYS; + r.freq = 1; + nstart = next_wday(start, wday); + } else if (*w < -6) { + /* + * A single ocurrence counting backwards from + * the end of the month. + */ + order = -(*w) / WEEKINDAYS; + wday = -(*w) % WEEKINDAYS; + nbwd = wday_per_month(tm_day.tm_mon + 1, + tm_day.tm_year + 1900, + wday); + if (nbwd < order) + return 0; + r.freq = nbwd - order + 1; + tm_start.tm_mday = 1; + tm_start.tm_mon = tm_day.tm_mon; + tm_start.tm_year = tm_day.tm_year; + tm_start.tm_isdst = -1; + nstart = date_sec_change( + next_wday(mktime(&tm_start), wday), + 0, + -WEEKINDAYS + ); + r.until = date_sec_change( + DAY(nstart), + 0, + r.freq * WEEKINDAYS + ); + if (rpt->until && r.until > rpt->until) + return 0; + } else + EXIT(_("illegal BYDAY value")); + + if (test_occurrence(nstart, dur, &r, exc, + start, day, occurrence)) + return 1; + } + } + else + return NO_EXPANSION; + + /* No occurrence */ + return 0; +} + +static int expand_yearly(time_t start, long dur, struct rpt *rpt, llist_t *exc, + time_t day, time_t *occurrence) +{ + struct tm tm_start, tm_day; + llist_item_t *i, *j; + int *m, *w, mday, wday, order, nbwd; + time_t nstart; + struct rpt r; + + localtime_r(&day, &tm_day); + /* + * The following five conditional alternatives are mutually exclusive + * and cover all eight cases of three booleans. + */ + /* BYMONTH expansion */ + if (rpt->bymonth.head && !rpt->bymonthday.head && !rpt->bywday.head) { + LLIST_FOREACH(&rpt->bymonth, i) { + m = LLIST_GET_DATA(i); + + /* Modify rrule start with new month. */ + localtime_r(&start, &tm_start); + tm_start.tm_mon = *m - 1; + tm_start.tm_isdst = -1; + nstart = mktime(&tm_start); + if (!date_chk(nstart, *m - 1, tm_start.tm_mday)) + continue; + if (find_occurrence(nstart, dur, rpt, exc, day, + occurrence)) + return 1; + } + } else + /* BYDAY special expansion for MONTHLY or YEARLY */ + if (!rpt->bymonthday.head && rpt->bywday.head) { + /* Check needed because frequency is modified later. */ + if (!freq_chk(day, start, dur, rpt, exc)) + return 0; + + LLIST_FOREACH(&rpt->bywday, i) { + w = LLIST_GET_DATA(i); + + localtime_r(&start, &tm_start); + /* + * Construct a suitable weekly rrule. BYMONTH + * reduction in find_occurrence() will limit + * occurrences if needed. + */ + r = *rpt; + r.type = RECUR_WEEKLY; + if (*w > 6) { + /* + * Special expand: A single ocurrence counting + * forward from the start of the month/year. + * Start in the week before with a frequency + * that matches the ordered weekday and with + * until day that allows only one occurrence. + */ + order = *w / WEEKINDAYS; + wday = *w % WEEKINDAYS; + if (rpt->bymonth.head) + nbwd = wday_per_month( + tm_day.tm_mon + 1, + tm_day.tm_year + 1900, + wday + ); + else + nbwd = wday_per_year( + tm_day.tm_year + 1900, + wday + ); + if (nbwd < order) + return 0; + r.freq = order; + tm_start.tm_mday = 1; + if (rpt->bymonth.head) + tm_start.tm_mon = tm_day.tm_mon; + else + tm_start.tm_mon = 0; + tm_start.tm_year = tm_day.tm_year; + tm_start.tm_isdst = -1; + nstart = date_sec_change( + next_wday(mktime(&tm_start), wday), + 0, + -WEEKINDAYS + ); + r.until = date_sec_change( + DAY(nstart), + 0, + r.freq * WEEKINDAYS + ); + if (rpt->until && r.until > rpt->until) + return 0; + } else if (*w > -1) { + /* Expand to each week of the month/year. */ + wday = *w % WEEKINDAYS; + r.freq = 1; + nstart = next_wday(start, wday); + } else if (*w < -6) { + /* + * Special expand: A single ocurrence counting + * backward from the end of the month/year. + */ + order = -(*w) / WEEKINDAYS; + wday = -(*w) % WEEKINDAYS; + if (rpt->bymonth.head) + nbwd = wday_per_month( + tm_day.tm_mon + 1, + tm_day.tm_year + 1900, + wday + ); + else + nbwd = wday_per_year( + tm_day.tm_year + 1900, + wday + ); + if (nbwd < order) + return 0; + r.freq = nbwd - order + 1; + tm_start.tm_mday = 1; + if (rpt->bymonth.head) + tm_start.tm_mon = tm_day.tm_mon; + else + tm_start.tm_mon = 0; + tm_start.tm_year = tm_day.tm_year; + tm_start.tm_isdst = -1; + nstart = date_sec_change( + next_wday(mktime(&tm_start), wday), + 0, + -WEEKINDAYS + ); + r.until = date_sec_change( + DAY(nstart), + 0, + r.freq * WEEKINDAYS + ); + if (rpt->until && r.until > rpt->until) + return 0; + } else + EXIT(_("illegal BYDAY value")); + + if (test_occurrence(nstart, dur, &r, exc, + start, day, occurrence)) + return 1; + } + } else + /* BYMONTHDAY expansion */ + if (!rpt->bymonth.head && rpt->bymonthday.head) { + LLIST_FOREACH(&rpt->bymonthday, i) { + mday = *(int *)LLIST_GET_DATA(i); + if (mday < 0) + mday = opp_mday( + tm_day.tm_year + 1900, + tm_day.tm_mon + 1, mday + ); + /* Modify rrule start with new monthday. */ + localtime_r(&start, &tm_start); + tm_start.tm_mday = mday; + tm_start.tm_isdst = -1; + nstart = mktime(&tm_start); + if (!date_chk(nstart, tm_start.tm_mon, mday)) + continue; + if (find_occurrence(nstart, dur, rpt, exc, day, + occurrence)) + return 1; + } + } else + /* BYMONTH and BYMONTHDAY expansion */ + if (rpt->bymonth.head && rpt->bymonthday.head) { + LLIST_FOREACH(&rpt->bymonth, i) { + m = LLIST_GET_DATA(i); + + LLIST_FOREACH(&rpt->bymonthday, j) { + mday = *(int *)LLIST_GET_DATA(j); + if (mday < 0) + mday = opp_mday( + tm_day.tm_year + 1900, + tm_day.tm_mon + 1, mday + ); + /* Modify start with new monthday and month. */ + localtime_r(&start, &tm_start); + /* Number of days in February! */ + if (*m == 2 && mday == 29 && + !ISLEAP(tm_start.tm_year + 1900) && + rpt->freq % 4) { + if (!freq_chk(day, start, dur, rpt, exc)) + return 0; + tm_start.tm_year -= tm_start.tm_year % 4; + } + tm_start.tm_mday = mday; + tm_start.tm_mon = *m - 1; + tm_start.tm_isdst = -1; + nstart = mktime(&tm_start); + if (!date_chk(nstart, *m - 1, mday)) + continue; + if (find_occurrence(nstart, dur, rpt, exc, day, + occurrence)) + return 1; + } + } + } else + return NO_EXPANSION; + + /* No occurrence */ + return 0; +} + +/* + * Membership test for the recurrence set of the rrule (start, dur, rpt, exc). + * + * Return true if day belongs to the set. If so, the occurrence is saved in a + * buffer. A positive result is always the outcome of find_occurrence(), whereas + * a negative result may be arrived at in other ways. + * + * The basic (type, frequency)-check is in find_occurrence(). When recurrence + * set expansion and/or reduction (RFC 5545) is needed, expansion is done before + * call of find_occurrence(), while reduction takes place in find_occurrence(). + * + * Recurrence set expansion is accomplished by a combination of calls of + * find_occurrence(), possibly with change of type, frequency and start. + */ +unsigned +recur_item_find_occurrence(time_t start, long dur, struct rpt *rpt, llist_t *exc, + time_t day, time_t *occurrence) +{ + int res; + + /* To make it possible to set an earlier start without expanding the + * recurrence set. */ + if (date_cmp_day(day, start) < 0) + return 0; + + switch (rpt->type) { + case RECUR_DAILY: + res = NO_EXPANSION; + break; + case RECUR_WEEKLY: + res = expand_weekly(start, dur, rpt, exc, day, occurrence); + break; + case RECUR_MONTHLY: + res = expand_monthly(start, dur, rpt, exc, day, occurrence); + break; + case RECUR_YEARLY: + res = expand_yearly(start, dur, rpt, exc, day, occurrence); + break; + default: + res = 0; + } + + if (res == NO_EXPANSION) + return find_occurrence(start, dur, rpt, exc, day, occurrence); + + /* The result of find_occurrence() is passed on. */ + return res; +} +#undef NO_EXPANSION + unsigned recur_apoint_find_occurrence(struct recur_apoint *rapt, time_t day_start, time_t *occurrence) { - return recur_item_find_occurrence(rapt->start, rapt->dur, - &rapt->exc, rapt->rpt->type, - rapt->rpt->freq, - rapt->rpt->until, day_start, - occurrence); + return recur_item_find_occurrence(rapt->start, rapt->dur, rapt->rpt, + &rapt->exc, day_start, occurrence); } unsigned recur_event_find_occurrence(struct recur_event *rev, time_t day_start, time_t *occurrence) { - return recur_item_find_occurrence(rev->day, -1, &rev->exc, - rev->rpt->type, rev->rpt->freq, - rev->rpt->until, day_start, - occurrence); + return recur_item_find_occurrence(rev->day, -1, rev->rpt, &rev->exc, + day_start, occurrence); } /* Check if a recurrent item belongs to the selected day. */ unsigned -recur_item_inday(time_t item_start, long item_dur, llist_t * item_exc, - int rpt_type, int rpt_freq, time_t rpt_until, +recur_item_inday(time_t start, long dur, + struct rpt *rpt, llist_t * exc, time_t day_start) { /* We do not need the (real) start time of the occurrence here, so just * ignore the buffer. */ - return recur_item_find_occurrence(item_start, item_dur, item_exc, - rpt_type, rpt_freq, rpt_until, + return recur_item_find_occurrence(start, dur, rpt, exc, day_start, NULL); } unsigned recur_apoint_inday(struct recur_apoint *rapt, time_t *day_start) { - return recur_item_inday(rapt->start, rapt->dur, &rapt->exc, - rapt->rpt->type, rapt->rpt->freq, - rapt->rpt->until, *day_start); + return recur_item_inday(rapt->start, rapt->dur, rapt->rpt, &rapt->exc, + *day_start); } unsigned recur_event_inday(struct recur_event *rev, time_t *day_start) { - return recur_item_inday(rev->day, -1, &rev->exc, - rev->rpt->type, rev->rpt->freq, - rev->rpt->until, *day_start); + return recur_item_inday(rev->day, -1, rev->rpt, &rev->exc, + *day_start); } /* Add an exception to a recurrent event. */ @@ -960,6 +1618,62 @@ void recur_apoint_erase(struct recur_apoint *rapt) LLIST_TS_UNLOCK(&recur_alist_p); } +/* Read monthday list. */ +void recur_bymonthday(llist_t *l, FILE *data_file) +{ + int c = 0, d; + + LLIST_INIT(l); + while ((c = getc(data_file)) == 'd') { + ungetc(c, data_file); + if (fscanf(data_file, "d%d ", &d) != 1) + EXIT(_("syntax error in bymonthday")); + int *i = mem_malloc(sizeof(int)); + *i = d; + LLIST_ADD(l, i); + } + ungetc(c, data_file); +} + +/* Read weekday list. */ +void recur_bywday(enum recur_type type, llist_t *l, FILE *data_file) +{ + int c = 0, w; + + type = !(type == RECUR_MONTHLY || type == RECUR_YEARLY); + + LLIST_INIT(l); + while ((c = getc(data_file)) == 'w') { + ungetc(c, data_file); + if (fscanf(data_file, "w%d ", &w) != 1) + EXIT(_("syntax error in bywday")); + if (type && (w < 0 || w > 6)) + EXIT(_("illegal BYDAY value")); + int *i = mem_malloc(sizeof(int)); + *i = w; + LLIST_ADD(l, i); + } + ungetc(c, data_file); +} + +/* Read month list. */ +void recur_bymonth(llist_t *l, FILE *data_file) +{ + int c = 0, m; + + LLIST_INIT(l); + while ((c = getc(data_file)) == 'm') { + ungetc(c, data_file); + if (fscanf(data_file, "m%d ", &m) != 1) + EXIT(_("syntax error in bymonth")); + EXIT_IF(m < 1 || m > 12, _("illegal bymonth value")); + int *i = mem_malloc(sizeof(int)); + *i = m; + LLIST_ADD(l, i); + } + ungetc(c, data_file); +} + /* * Read days for which recurrent items must not be repeated * (such days are called exceptions). @@ -989,6 +1703,7 @@ void recur_exc_scan(llist_t * lexc, FILE * data_file) exc->st = mktime(&day); LLIST_ADD(lexc, exc); } + ungetc(c, data_file); } /* @@ -1088,3 +1803,75 @@ void recur_apoint_paste_item(struct recur_apoint *rapt, time_t date) if (notify_bar()) notify_check_repeated(rapt); } + +/* + * Finds the next occurrence of a recurrent item and returns it in the provided + * buffer. Useful for test of a repeated item. + */ +int recur_next_occurrence(time_t s, long d, struct rpt *r, llist_t *e, + time_t day, time_t *next) +{ + int ret = 0; + + if (r->until && r->until <= day) + return ret; + + while (!r->until || day < r->until) { + day = NEXTDAY(day); + if (!check_sec(&day)) + break; + if (recur_item_find_occurrence(s, d, r, e, day, next)) { + /* Multi-day appointment. */ + if (*next < day) + continue; + ret = 1; + break; + } + } + return ret; +} + +/* + * Finds the nth occurrence (incl. start) of a recurrence rule (s, d, r, e) + * and returns it in the provided buffer. + */ +int recur_nth_occurrence(time_t s, long d, struct rpt *r, llist_t *e, int n, + time_t *nth) +{ + time_t day; + + if (n <= 0) + return 0; + + for (n--, *nth = s; n > 0; n--) { + day = DAY(*nth); + if (!recur_next_occurrence(s, d, r, e, day, nth)) + break; + } + return !n; +} + +/* + * Finds the previous occurrence - the most recent before day - and returns it + * in the provided buffer. + */ +int recur_prev_occurrence(time_t s, long d, struct rpt *r, llist_t *e, + time_t day, time_t *prev) +{ + int ret = 0; + + if (day <= DAY(s)) + return ret; + + while (DAY(s) < day) { + day = PREVDAY(day); + if (recur_item_find_occurrence(s, d, r, e, day, prev)) { + /* Multi-day appointment. */ + if (d != -1 && *prev < day && day < *prev + d) + continue; + ret = 1; + break; + } + } + return ret; +} @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -43,6 +43,7 @@ #include <stdlib.h> #include <string.h> +#include "config.h" #include "sha1.h" #define rol(val, n) (((val) << (n)) | ((val) >> (32 - (n)))) @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -52,6 +52,6 @@ typedef struct { void sha1_init(sha1_ctx_t *); void sha1_update(sha1_ctx_t *, const uint8_t *, unsigned int); -void sha1_final(sha1_ctx_t *, uint8_t *); +void sha1_final(sha1_ctx_t *, uint8_t[SHA1_DIGESTLEN]); void sha1_digest(const char *, char *); void sha1_stream(FILE *, char *); @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -68,27 +68,18 @@ /* * General signal handling routine. - * Catch return values from children (user-defined notification commands). - * This is needed to avoid zombie processes running on system. - * Also catch CTRL-C (SIGINT), and SIGWINCH to resize screen automatically. + * Catch SIGWINCH to resize screen automatically. */ static void generic_hdlr(int sig) { switch (sig) { - case SIGCHLD: - while (waitpid(WAIT_MYPGRP, NULL, WNOHANG) > 0) ; - break; case SIGWINCH: resize = 1; clearok(curscr, TRUE); ungetch(KEY_RESIZE); break; case SIGTERM: - if (unlink(path_cpid) != 0) { - EXIT(_("Could not remove calcurse lock file: %s\n"), - strerror(errno)); - } - exit(EXIT_SUCCESS); + exit_calcurse(EXIT_SUCCESS); break; case SIGUSR1: want_reload = 1; @@ -117,12 +108,11 @@ unsigned sigs_set_hdlr(int sig, void (*handler) (int)) /* Signal handling init. */ void sigs_init() { - if (!sigs_set_hdlr(SIGCHLD, generic_hdlr) - || !sigs_set_hdlr(SIGWINCH, generic_hdlr) + if (!sigs_set_hdlr(SIGWINCH, generic_hdlr) || !sigs_set_hdlr(SIGTERM, generic_hdlr) || !sigs_set_hdlr(SIGUSR1, generic_hdlr) || !sigs_set_hdlr(SIGINT, SIG_IGN)) - exit_calcurse(1); + exit_calcurse(EXIT_FAILURE); } /* Ignore SIGWINCH and SIGTERM signals. */ diff --git a/src/strings.c b/src/strings.c index b3dc1c4..45c9310 100644 --- a/src/strings.c +++ b/src/strings.c @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -158,9 +158,7 @@ void todo_delete(struct todo *todo) */ void todo_resort(struct todo *t) { - llist_item_t *i = LLIST_FIND_FIRST(&todolist, t, NULL); - LLIST_REMOVE(&todolist, i); - LLIST_ADD_SORTED(&todolist, t, todo_cmp); + LLIST_REORDER(&todolist, t, todo_cmp); } /* Flag a todo item. */ diff --git a/src/ui-calendar.c b/src/ui-calendar.c index 4a8ef44..c1719d8 100644 --- a/src/ui-calendar.c +++ b/src/ui-calendar.c @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -45,14 +45,14 @@ #include "calcurse.h" static struct date today, slctd_day; -static unsigned ui_calendar_view, week_begins_on_monday; +static unsigned ui_calendar_view; +static int wday_start; /* this is used in signed arithmetic */ static pthread_mutex_t date_thread_mutex = PTHREAD_MUTEX_INITIALIZER; -static void draw_monthly_view(struct scrollwin *, struct date *, unsigned); -static void draw_weekly_view(struct scrollwin *, struct date *, unsigned); -static void (*draw_calendar[CAL_VIEWS]) (struct scrollwin *, struct date *, - unsigned) = { -draw_monthly_view, draw_weekly_view}; +static void draw_monthly_view(struct scrollwin *, struct date *); +static void draw_weekly_view(struct scrollwin *, struct date *); +static void (*draw_calendar[CAL_VIEWS]) (struct scrollwin *, + struct date *) = {draw_monthly_view, draw_weekly_view}; /* Six weeks cover a month. */ static int monthly_view_cache[WEEKINDAYS * 6]; @@ -148,30 +148,26 @@ struct date *ui_calendar_get_today(void) /* Needed to display sunday or monday as the first day of week in calendar. */ void ui_calendar_set_first_day_of_week(enum wday first_day) { - switch (first_day) { - case SUNDAY: - week_begins_on_monday = 0; - break; - case MONDAY: - week_begins_on_monday = 1; - break; - default: + if (first_day >= 0 && first_day <= 6) + wday_start = first_day; + else { ERROR_MSG(_("ERROR setting first day of week")); - week_begins_on_monday = 0; - /* NOTREACHED */ + wday_start = 0; } } /* Swap first day of week in calendar. */ void ui_calendar_change_first_day_of_week(void) { - week_begins_on_monday = !week_begins_on_monday; + wday_start++; + if(wday_start >= WEEKINDAYS) + wday_start = 0; } /* Return 1 if week begins on monday, 0 otherwise. */ -unsigned ui_calendar_week_begins_on_monday(void) +int ui_calendar_get_wday_start(void) { - return week_begins_on_monday; + return wday_start; } /* Fill in the given variable with the current date. */ @@ -219,18 +215,14 @@ void ui_calendar_monthly_view_cache_set_invalid(void) monthly_view_cache_valid = 0; } -static int weeknum(const struct tm *t, int firstweekday) +static int weeknum(const struct tm *t, int wday_start) { int wday, wnum; wday = t->tm_wday; - if (firstweekday == MONDAY) { - if (wday == SUNDAY) - wday = 6; - else - wday--; - } - wnum = ((t->tm_yday + WEEKINDAYS - wday) / WEEKINDAYS); + wnum = ((t->tm_yday + WEEKINDAYS + -modify_wday(wday, -wday_start)) + / WEEKINDAYS); + if (wnum < 0) wnum = 0; @@ -296,7 +288,7 @@ static int ISO8601weeknum(const struct tm *t) * Return the tm structure for the first day of the first week * (containing a day) of the selected month. */ -static struct tm get_first_day(unsigned sunday_first) +static struct tm get_first_day(int wday_start) { struct tm t; struct date d; @@ -308,26 +300,20 @@ static struct tm get_first_day(unsigned sunday_first) t = date2tm(d, 0, 0); mktime(&t); /* get the first day of the week */ - date_change(&t, 0, - -(sunday_first ? - t.tm_wday : - (t.tm_wday + WEEKINDAYS - 1) % WEEKINDAYS)); + date_change(&t, 0, -modify_wday(t.tm_wday, -wday_start)); + return t; } -static struct tm get_first_weekday(unsigned sunday_first) +static struct tm get_first_weekday(int wday_start) { - int c_wday, days_to_remove; + int c_wday; struct tm t; c_wday = ui_calendar_get_wday(&slctd_day); - if (sunday_first) - days_to_remove = c_wday; - else - days_to_remove = c_wday == 0 ? WEEKINDAYS - 1 : c_wday - 1; - t = date2tm(slctd_day, 0, 0); - date_change(&t, 0, -days_to_remove); + + date_change(&t, 0, -modify_wday(c_wday, -wday_start)); return t; } @@ -346,8 +332,7 @@ static void draw_week_number(struct scrollwin *sw, struct tm t) /* Draw the monthly view inside calendar panel. */ static void -draw_monthly_view(struct scrollwin *sw, struct date *current_day, - unsigned sunday_first) +draw_monthly_view(struct scrollwin *sw, struct date *current_day) { struct date c_day; int slctd, w_day, numdays, j, week = 0; @@ -373,7 +358,7 @@ draw_monthly_view(struct scrollwin *sw, struct date *current_day, * Step forward by week until past the last day of the month. * The first day of the first week may belong to the previous month. */ - t = t_first = get_first_day(sunday_first); + t = t_first = get_first_day(wday_start); t.tm_mday += WEEKINDAYS; mktime(&t); last_day += WEEKINDAYS; @@ -423,7 +408,7 @@ draw_monthly_view(struct scrollwin *sw, struct date *current_day, custom_apply_attr(sw->inner, ATTR_HIGHEST); for (j = 0; j < WEEKINDAYS; j++) { mvwaddstr(sw->inner, ofs_y, ofs_x + weekw + 4 * j, - nl_langinfo(ABDAY_1 + (1 + j - sunday_first) % WEEKINDAYS)); + nl_langinfo(ABDAY_1 + modify_wday(j, wday_start))); } custom_remove_attr(sw->inner, ATTR_HIGHEST); WINS_CALENDAR_UNLOCK; @@ -449,11 +434,9 @@ draw_monthly_view(struct scrollwin *sw, struct date *current_day, if (j == first_day || (mo == 1 && j == WEEKINDAYS) || (mo == 12 && j >= 4 * WEEKINDAYS)) { - if (sunday_first) - date_change(&t, 0, 1); + date_change(&t, 0, WDAY(MONDAY)); week = ISO8601weeknum(&t); - if (sunday_first) - date_change(&t, 0, -1); + date_change(&t, 0, -WDAY(MONDAY)); } else week++; } @@ -506,8 +489,7 @@ draw_monthly_view(struct scrollwin *sw, struct date *current_day, /* Draw the weekly view inside calendar panel. */ static void -draw_weekly_view(struct scrollwin *sw, struct date *current_day, - unsigned sunday_first) +draw_weekly_view(struct scrollwin *sw, struct date *current_day) { #define DAYSLICESNO 6 const int WCALWIDTH = 28; @@ -520,14 +502,14 @@ draw_weekly_view(struct scrollwin *sw, struct date *current_day, OFFX = (wins_sbar_width() - 2 - WCALWIDTH) / 2; /* Print the week number, calculated from monday. */ - t = get_first_weekday(0); + t = get_first_weekday(MONDAY); draw_week_number(sw, t); /* Now draw calendar view. */ for (j = 0; j < WEEKINDAYS; j++) { /* get next day */ if (j == 0) - t = get_first_weekday(sunday_first); + t = get_first_weekday(wday_start); else date_change(&t, 0, 1); @@ -538,7 +520,7 @@ draw_weekly_view(struct scrollwin *sw, struct date *current_day, /* print the day names, with regards to the first day of the week */ custom_apply_attr(sw->inner, ATTR_HIGHEST); mvwaddstr(sw->inner, OFFY, OFFX + 4 * j, - nl_langinfo(ABDAY_1 + (1 + j - sunday_first) % WEEKINDAYS)); + nl_langinfo(ABDAY_1 + modify_wday(j, wday_start))); custom_remove_attr(sw->inner, ATTR_HIGHEST); /* Check if the day to be printed has an item or not. */ @@ -578,9 +560,8 @@ draw_weekly_view(struct scrollwin *sw, struct date *current_day, if (j != WEEKINDAYS - 1 && i != DAYSLICESNO - 1) { WINS_CALENDAR_LOCK; - mvwhline(sw->inner, OFFY + 2 + i, - OFFX + 3 + 4 * j, ACS_S9, - 2); + mvwaddstr(sw->inner, OFFY + 2 + i, + OFFX + 3 + 4 * j, "__"); WINS_CALENDAR_UNLOCK; } if (slices[i]) { @@ -611,9 +592,9 @@ draw_weekly_view(struct scrollwin *sw, struct date *current_day, /* Draw marks to indicate midday on the sides of the calendar. */ WINS_CALENDAR_LOCK; custom_apply_attr(sw->inner, ATTR_HIGHEST); - mvwhline(sw->inner, OFFY + 1 + DAYSLICESNO / 2, OFFX, ACS_S9, 1); - mvwhline(sw->inner, OFFY + 1 + DAYSLICESNO / 2, - OFFX + WCALWIDTH - 1, ACS_S9, 1); + mvwaddch(sw->inner, OFFY + 1 + DAYSLICESNO / 2, OFFX, '<'); + mvwaddch(sw->inner, OFFY + 1 + DAYSLICESNO / 2, + OFFX + WCALWIDTH - 1, '>'); custom_remove_attr(sw->inner, ATTR_HIGHEST); WINS_CALENDAR_UNLOCK; @@ -624,11 +605,9 @@ draw_weekly_view(struct scrollwin *sw, struct date *current_day, void ui_calendar_update_panel(void) { struct date current_day; - unsigned sunday_first; ui_calendar_store_current_date(¤t_day); - sunday_first = !ui_calendar_week_begins_on_monday(); - draw_calendar[ui_calendar_view] (&sw_cal, ¤t_day, sunday_first); + draw_calendar[ui_calendar_view] (&sw_cal, ¤t_day); wins_scrollwin_display(&sw_cal, NOHILT); } @@ -728,28 +707,14 @@ void ui_calendar_move(enum move move, int count) ret = date_change(&t, count * YEARINMONTHS, 0); break; case WEEK_START: - /* Normalize struct tm to get week day number. */ mktime(&t); - if (ui_calendar_week_begins_on_monday()) - days_to_remove = - ((t.tm_wday == - 0) ? WEEKINDAYS - 1 : t.tm_wday - 1); - else - days_to_remove = - ((t.tm_wday == 0) ? 0 : t.tm_wday); + days_to_remove = WDAY(t.tm_wday); days_to_remove += (count - 1) * WEEKINDAYS; ret = date_change(&t, 0, -days_to_remove); break; case WEEK_END: mktime(&t); - if (ui_calendar_week_begins_on_monday()) - days_to_add = - ((t.tm_wday == - 0) ? 0 : WEEKINDAYS - t.tm_wday); - else - days_to_add = ((t.tm_wday == 0) ? - WEEKINDAYS - 1 : WEEKINDAYS - 1 - - t.tm_wday); + days_to_add = modify_wday(-t.tm_wday, wday_start - 1); days_to_add += (count - 1) * WEEKINDAYS; ret = date_change(&t, 0, days_to_add); break; diff --git a/src/ui-day.c b/src/ui-day.c index 7d571fc..6a038fa 100644 --- a/src/ui-day.c +++ b/src/ui-day.c @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -34,6 +34,8 @@ * */ +#include <limits.h> +#include <langinfo.h> #include "calcurse.h" /* Cut & paste registers. */ @@ -77,7 +79,7 @@ void ui_day_find_sel(void) */ time_t ui_day_sel_date(void) { - return update_time_in_date(ui_day_get_sel()->order, 0, 0); + return DAY(ui_day_get_sel()->order); } /* @@ -163,32 +165,44 @@ static time_t day_edit_time(time_t start, long duration, int move) /* * Change start time or move an item. * Input/output: start and dur. + * For recurrent items the new start time must match the repetition pattern. * If move = 0, end time is fixed, and the new duration is calculated * when the new start time is known. * If move = 1, duration is fixed, but passed on for validation of new end time. */ -static void update_start_time(time_t *start, long *dur, int move) +static void update_start_time(time_t *start, long *dur, struct rpt *rpt, int move) { time_t newtime; const char *msg_wrong_time = _("Invalid time: start time must come before end time!"); + char *msg_match = + _("Repetition must begin on start day (%s)."); const char *msg_enter = _("Press [Enter] to continue"); + char *msg; for (;;) { newtime = day_edit_time(*start, *dur, move); if (!newtime) break; - if (move) { - *start = newtime; - break; + if (rpt && !recur_item_find_occurrence(newtime, *dur, rpt, NULL, + DAY(newtime), + NULL)) { + msg = day_ins(&msg_match, newtime); + status_mesg(msg, msg_enter); + mem_free(msg); } else { - if (newtime <= *start + *dur) { - *dur -= (newtime - *start); + if (move) { *start = newtime; break; + } else { + if (newtime <= *start + *dur) { + *dur -= (newtime - *start); + *start = newtime; + break; + } } + status_mesg(msg_wrong_time, msg_enter); } - status_mesg(msg_wrong_time, msg_enter); keys_wgetch(win[KEY].p); } return; @@ -273,8 +287,8 @@ static void update_desc(char **desc) updatestring(win[STA].p, desc, 0, 1); } -/* Edit the list of exception days for a recurrent item. */ -static int update_exc(llist_t *exc) +/* Edit a list of exception days for a recurrent item. */ +static int edit_exc(llist_t *exc) { int updated = 0; @@ -288,7 +302,7 @@ static int update_exc(llist_t *exc) while (1) { ret = updatestring(win[STA].p, &days, 0, 1); if (ret == GETSTRING_VALID || ret == GETSTRING_RET) { - if (recur_update_exc(exc, days)) { + if (recur_str2exc(exc, days)) { updated = 1; break; } else { @@ -304,97 +318,446 @@ static int update_exc(llist_t *exc) return updated; } -static void update_rept(struct rpt **rpt, const time_t start, llist_t *exc) +/* + * Decode an integer representing a weekday or ordered weekday. + * The return value is the (abbreviated) localized day name. + * The order is returned in the second argument. + */ +static char *int2wday(int i, int *ord, int_list_t type) +{ + if (type == BYDAY_W || + ((type == BYDAY_M || type == BYDAY_Y) && -1 < i && i < 7)) + *ord = 0; + else if ((type == BYDAY_M && 6 < i && i < 42) || + (type == BYDAY_Y && 6 < i && i < 378)) + *ord = i / 7; + else if ((type == BYDAY_M && -42 < i && i < -6) || + (type == BYDAY_Y && -378 < i && i < -6)) { + i = -i; + *ord = -(i / 7); + } else + return NULL; + + return nl_langinfo(ABDAY_1 + i % 7); +} + +/* + * Given a (linked) list of integers representing weekdays, monthdays or months. + * Return a string containing the weekdays or integers separated by spaces. + */ +static char *int2str(llist_t *il, int_list_t type) +{ + llist_item_t *i; + int *p, ord = 0; + char *wday; + struct string s; + + string_init(&s); + LLIST_FOREACH(il, i) { + p = LLIST_GET_DATA(i); + wday = int2wday(*p, &ord, type); + if (wday) + string_catf(&s, ord ? "%d%s " : "%.0d%s ", ord, wday); + else + string_catf(&s, "%i ", *p); + } + + return string_buf(&s); +} + +/* + * Encode a weekday or ordered weekday as an integer. + */ +static int wday2int(char *s) +{ + int i, ord; + char *tail; + + i = strtol(s, &tail, 10); + if (!i && tail == s) + ord = 0; + else + ord = i > 0 ? i : -i; + + if (!strcmp(tail, nl_langinfo(ABDAY_1))) + return (i < 0 ? -1 : 1) * (ord * 7 + 0); + else if (!strcmp(tail, nl_langinfo(ABDAY_2))) + return (i < 0 ? -1 : 1) * (ord * 7 + 1); + else if (!strcmp(tail, nl_langinfo(ABDAY_3))) + return (i < 0 ? -1 : 1) * (ord * 7 + 2); + else if (!strcmp(tail, nl_langinfo(ABDAY_4))) + return (i < 0 ? -1 : 1) * (ord * 7 + 3); + else if (!strcmp(tail, nl_langinfo(ABDAY_5))) + return (i < 0 ? -1 : 1) * (ord * 7 + 4); + else if (!strcmp(tail, nl_langinfo(ABDAY_6))) + return (i < 0 ? -1 : 1) * (ord * 7 + 5); + else if (!strcmp(tail, nl_langinfo(ABDAY_7))) + return (i < 0 ? -1 : 1) * (ord * 7 + 6); + else + return -1; +} + +/* + * Parse an integer or weekday string. Valid values depend on type. + * On success the integer or integer code is returned in *i. + */ +static int parse_int(char *s, long *i, int_list_t type) +{ + char *eos; + + if (type == BYDAY_W || type == BYDAY_M || type == BYDAY_Y) { + *i = wday2int(s); + if (*i == -1) + return 0; + } else { + *i = strtol(s, &eos, 10); + if (*eos || *i > INT_MAX) + return 0; + } + + switch (type) { + case BYMONTH: + /* 1,..,12 */ + if (0 < *i && *i < 13) + return 1; + break; + case BYDAY_W: + /* 0,..,6 */ + if (-1 < *i && *i < 7) + return 1; + break; + case BYDAY_M: + /* 0,..,6 or 7,..,41 or -7,..,-41 */ + /* 41 = 5*7 + 6, i.e. fifth Saturday of the month */ + if ((-42 < *i && *i < -6) || (-1 < *i && *i < 42)) + return 1; + break; + case BYDAY_Y: + /* 0,..,6 or 7,..,377 or -7,..,-377 */ + /* 377 = 53*7 + 6, i.e. 53th Saturday of the year */ + if ((-378 < *i && *i < -6) || (-1 < *i && *i < 378)) + return 1; + break; + case BYMONTHDAY: + /* 1,..,31 or -1,..,-31 */ + if ((0 < *i && *i < 32) || (-32 < *i && *i < 0)) + return 1; + break; + default: + return 0; + } + return 0; +} + +/* + * Update a (linked) list of integer values from a string of such values. Any + * positive number of spaces are allowed before, between and after the values. + */ +static int str2int(llist_t *l, char *s, int type) { + int *j, updated = 0; + char *c; + long i; + llist_t nl; + LLIST_INIT(&nl); + + while (1) { + while (*s == ' ') + s++; + if ((c = strchr(s, ' '))) + *c = '\0'; + else if (!strlen(s)) + break; + if (parse_int(s, &i, type)) { + j = mem_malloc(sizeof(int)); + *j = i; + LLIST_ADD(&nl, j); + } else + goto cleanup; + if (c) + s = c + 1; + else + break; + } + recur_free_int_list(l); + recur_int_list_dup(l, &nl); + updated = 1; +cleanup: + recur_free_int_list(&nl); + return updated; +} + +static void help_ilist(int_list_t list, int rule) { - /* Pointers to dynamically allocated memory. */ - char *msg_rpt_current = NULL; - char *msg_rpt_asktype = NULL; + char *msg1 = ""; + char *msg2 = ""; + char *byday_w_d = _("Limit repetition to listed days."); + char *byday_w_w = _("Expand repetition to listed days."); + char *byday_m_m_1 = + _("Expand repetition to listed days, either all or 1st, 2nd, ... of month."); + char *byday_m_m_2 = + _("Note: limit to monthdays, if any."); + char *byday_y_y_1 = + _("Expand repetition to listed days, either all or 1st, 2nd, ... of year."); + char *byday_y_y_2 = + _("Note: expand to listed months, if any; limit to monthdays, if any."); + char *bymonth_dwm = + _("Limit repetition to listed months."); + char *bymonth_y = + _("Expand repetition to listed months."); + char *bymonthday_d = _("Limit repetition to listed days of month."); + char *bymonthday_my = _("Expand repetition to listed days of month."); + + + switch (list) { + case BYDAY_W: + switch (rule) { + case RECUR_DAILY: + msg1 = byday_w_d; + msg2 = ""; + break; + case RECUR_WEEKLY: + msg1 = byday_w_w; + msg2 = ""; + break; + default: + EXIT("internal inconsistency"); + } + break; + case BYDAY_M: + switch (rule) { + case RECUR_MONTHLY: + msg1 = byday_m_m_1; + msg2 = byday_m_m_2; + break; + default: + EXIT("internal inconsistency"); + } + break; + case BYDAY_Y: + switch (rule) { + case RECUR_YEARLY: + msg1 = byday_y_y_1; + msg2 = byday_y_y_2; + break; + default: + EXIT("internal inconsistency"); + } + break; + case BYMONTH: + switch (rule) { + case RECUR_DAILY: + case RECUR_WEEKLY: + case RECUR_MONTHLY: + msg1 = bymonth_dwm; + msg2 = ""; + break; + case RECUR_YEARLY: + msg1 = bymonth_y; + msg2 = ""; + break; + default: + break; + } + break; + case BYMONTHDAY: + switch (rule) { + case RECUR_DAILY: + msg1 = bymonthday_d; + msg2 = ""; + break; + case RECUR_MONTHLY: + case RECUR_YEARLY: + msg1 = bymonthday_my; + msg2 = ""; + break; + default: + break; + } + break; + default: + break; + } + status_mesg(msg1, msg2); + keys_wgetch(win[KEY].p); +} + +/* Edit an rrule (linked) list of integers. */ +static int edit_ilist(llist_t *ilist, int_list_t list_type, int rule_type) +{ + char *msg; + char *wday = NULL; + char *wday_w = _("Weekdays %s|..|%s, space-separated list, '?' for help:"); + char *wday_m = + _("Weekdays [n]%s|..|[n]%s, space-separated list, n=1,-1,..,5,-5, '?' for help:"); + char *wday_y = + _("Weekdays [n]%s|..|[n]%s, space-separated list, n=1,-1,..,53,-53, '?' for help:"); + char *month = _("Months 1|..|12, space-separated list, '?' for help:"); + char *mday = _("Monthdays 1|..|31 or -1|..|-31, space-separated list, '?' for help:"); + char *invalid = _("Invalid format - try again."); + char *cont = _("Press any key to continue."); + int updated = 0; + + if (list_type == NOLL) + return !updated; + char *istr; + enum getstr ret; + + switch (list_type) { + case BYDAY_W: + asprintf(&wday, wday_w, + nl_langinfo(ABDAY_2), nl_langinfo(ABDAY_1)); + msg = wday; + break; + case BYDAY_M: + asprintf(&wday, wday_m, + nl_langinfo(ABDAY_2), nl_langinfo(ABDAY_1)); + msg = wday; + break; + case BYDAY_Y: + asprintf(&wday, wday_y, + nl_langinfo(ABDAY_2), nl_langinfo(ABDAY_1)); + msg = wday; + break; + case BYMONTH: + msg = month; + break; + case BYMONTHDAY: + msg = mday; + break; + default: + msg = NULL; + break; + } + status_mesg(msg, ""); + istr = int2str(ilist, list_type); + while (1) { + ret = updatestring(win[STA].p, &istr, 0, 1); + if (ret == GETSTRING_VALID || ret == GETSTRING_RET) { + if (*(istr + strlen(istr) - 1) == '?') + help_ilist(list_type, rule_type); + else if (str2int(ilist, istr, list_type)) { + updated = 1; + break; + } else { + status_mesg(invalid, cont); + keys_wgetch(win[KEY].p); + } + mem_free(istr); + status_mesg(msg, ""); + istr = int2str(ilist, list_type); + } else if (ret == GETSTRING_ESC) + break; + } + mem_free(istr); + mem_free(wday); + + return updated; +} + +static int update_rept(time_t start, long dur, struct rpt **rpt, llist_t *exc, + int simple) +{ + int updated = 0, count; + struct rpt nrpt; + time_t until; + char *types = NULL; char *freqstr = NULL; char *timstr = NULL; char *outstr = NULL; + const char *msg_cont = _("Press any key to continue."); + + LLIST_INIT(&nrpt.exc); + LLIST_INIT(&nrpt.bywday); + LLIST_INIT(&nrpt.bymonth); + LLIST_INIT(&nrpt.bymonthday); - /* Update repetition type. */ - int newtype; - const char *msg_rpt_prefix = _("Enter the new repetition type:"); - const char *msg_rpt_daily = _("(d)aily"); - const char *msg_rpt_weekly = _("(w)eekly"); - const char *msg_rpt_monthly = _("(m)onthly"); - const char *msg_rpt_yearly = _("(y)early"); + /* Edit repetition type. */ + const char *msg_prefix = _("Base period:"); + const char *daily = _("day"); + const char *weekly = _("week"); + const char *monthly = _("month"); + const char *yearly = _("year"); + const char *dwmy = _("[dwmy]"); /* Find the current repetition type. */ - const char *rpt_current; + const char *current; switch (recur_def2char((*rpt)->type)) { case 'D': - rpt_current = msg_rpt_daily; + current = daily; break; case 'W': - rpt_current = msg_rpt_weekly; + current = weekly; break; case 'M': - rpt_current = msg_rpt_monthly; + current = monthly; break; case 'Y': - rpt_current = msg_rpt_yearly; + current = yearly; break; default: - /* NOTREACHED, but makes the compiler happier. */ - rpt_current = msg_rpt_daily; + /* New item. */ + current = ""; } - asprintf(&msg_rpt_current, _("(currently using %s)"), rpt_current); - asprintf(&msg_rpt_asktype, "%s %s, %s, %s, %s? %s", msg_rpt_prefix, - msg_rpt_daily, msg_rpt_weekly, msg_rpt_monthly, - msg_rpt_yearly, msg_rpt_current); - const char *msg_rpt_choice = _("[dwmy]"); - switch (status_ask_choice(msg_rpt_asktype, msg_rpt_choice, 4)) { + asprintf(&types, "%s %s/%s/%s/%s?", + msg_prefix, daily, weekly, monthly, yearly); + if (current[0]) + asprintf(&types, "%s [%s]", types, current); + switch (status_ask_choice(types, dwmy, 4)) { case 1: - newtype = 'D'; + nrpt.type = recur_char2def('D'); break; case 2: - newtype = 'W'; + nrpt.type = recur_char2def('W'); break; case 3: - newtype = 'M'; + nrpt.type = recur_char2def('M'); break; case 4: - newtype = 'Y'; + nrpt.type = recur_char2def('Y'); break; + case -2: /* user typed RETURN */ + if (current[0]) { + nrpt.type = (*rpt)->type; + break; + } default: goto cleanup; } - /* Update frequency. */ - int newfreq; - const char *msg_wrong_freq = _("Invalid frequency."); - const char *msg_enter = _("Press [Enter] to continue"); + /* Edit frequency. */ + const char *msg_freq = _("Frequency:"); + const char *msg_inv_freq = _("Invalid frequency."); do { - status_mesg(_("Enter the repetition frequency:"), ""); + status_mesg(msg_freq, ""); mem_free(freqstr); asprintf(&freqstr, "%d", (*rpt)->freq); if (updatestring(win[STA].p, &freqstr, 0, 1) != GETSTRING_VALID) { goto cleanup; } - newfreq = atoi(freqstr); - if (newfreq == 0) { - status_mesg(msg_wrong_freq, msg_enter); + nrpt.freq = atoi(freqstr); + if (nrpt.freq <= 0) { + status_mesg(msg_inv_freq, msg_cont); keys_wait_for_any_key(win[KEY].p); } } - while (newfreq == 0); + while (nrpt.freq <= 0); - /* Update end date. */ - time_t newuntil; + /* Edit until date. */ const char *msg_until_1 = - _("Enter end date or duration ('?' for input formats):"); + _("Until date, increment or repeat count ('?' for input formats):"); const char *msg_help_1 = - _("Date: %s (year or month may be omitted). Endless duration: 0."); + _("Date: %s (year, month may be omitted, endless: 0)."); const char *msg_help_2 = - _("Duration in days: +dd. Duration in weeks and days: +??w??d."); - const char *msg_wrong_time = - _("Invalid date: end date must come after start date (%s)."); - const char *msg_wrong_date = _("Invalid date."); + _("Increment: +?? (days) or: +??w??d (weeks). " + "Repeat count: #?? (number)."); + const char *msg_inv_until = + _("Invalid date: until date must come after start date (%s)."); + const char *msg_inv_date = _("Invalid date."); + const char *msg_count = _("Repeat count is too big."); for (;;) { + count = 0; mem_free(timstr); if ((*rpt)->until) timstr = date_sec2date_str((*rpt)->until, DATEFMT(conf.input_datefmt)); @@ -404,7 +767,7 @@ static void update_rept(struct rpt **rpt, const time_t start, llist_t *exc) if (updatestring(win[STA].p, &timstr, 0, 1) == GETSTRING_ESC) goto cleanup; if (strcmp(timstr, "") == 0 || strcmp(timstr, "0") == 0) { - newuntil = 0; + nrpt.until = 0; break; } if (*(timstr + strlen(timstr) - 1) == '?') { @@ -416,56 +779,160 @@ static void update_rept(struct rpt **rpt, const time_t start, llist_t *exc) } if (*timstr == '+') { unsigned days; - if (!parse_date_duration(timstr + 1, &days, start)) { - status_mesg(msg_wrong_date, msg_enter); + if (!parse_date_increment(timstr + 1, &days, start)) { + status_mesg(msg_inv_date, msg_cont); keys_wgetch(win[KEY].p); continue; } /* Until is midnight of the day. */ - newuntil = date_sec_change( - update_time_in_date(start, 0, 0), - 0, days - ); + nrpt.until = date_sec_change(DAY(start), 0, days); + } else if (*timstr == '#') { + char *eos; + count = strtol(timstr + 1, &eos, 10); + if (*eos || !(count > 0)) + continue; + nrpt.until = 0; + if (!recur_nth_occurrence(start, dur, &nrpt, exc, + count, &until)) { + status_mesg(msg_count, msg_cont); + keys_wgetch(win[KEY].p); + continue; + } + nrpt.until = DAY(until); + break; } else { int year, month, day; if (!parse_date(timstr, conf.input_datefmt, &year, &month, &day, ui_calendar_get_slctd_day())) { - status_mesg(msg_wrong_date, msg_enter); + status_mesg(msg_inv_date, msg_cont); keys_wgetch(win[KEY].p); continue; } struct date d = { day, month, year }; - newuntil = date2sec(d, 0, 0); + nrpt.until = date2sec(d, 0, 0); } /* Conmpare days (midnights) - until-day may equal start day. */ - if (newuntil >= update_time_in_date(start, 0, 0)) + if (nrpt.until >= DAY(start)) break; mem_free(timstr); mem_free(outstr); timstr = date_sec2date_str(start, DATEFMT(conf.input_datefmt)); - asprintf(&outstr, msg_wrong_time, timstr); - status_mesg(outstr, msg_enter); + asprintf(&outstr, msg_inv_until, timstr); + status_mesg(outstr, msg_cont); keys_wgetch(win[KEY].p); } - /* Update exception list. */ - if (!update_exc(exc)) + if (simple) { + (*rpt)->type = nrpt.type; + (*rpt)->freq = nrpt.freq; + (*rpt)->until = nrpt.until; + updated = 1; + goto cleanup; + } + + /* Edit exception list. */ + recur_exc_dup(&nrpt.exc, exc); + if (!edit_exc(&nrpt.exc)) + goto cleanup; + + /* Edit BYDAY list. */ + int_list_t byday_type; + switch (nrpt.type) { + case RECUR_DAILY: + byday_type = BYDAY_W; + break; + case RECUR_WEEKLY: + byday_type = BYDAY_W; + break; + case RECUR_MONTHLY: + byday_type = BYDAY_M; + break; + case RECUR_YEARLY: + byday_type = BYDAY_Y; + break; + default: + byday_type = NOLL; + break; + } + recur_int_list_dup(&nrpt.bywday, &(*rpt)->bywday); + if (!edit_ilist(&nrpt.bywday, byday_type, nrpt.type)) + goto cleanup; + + /* Edit BYMONTH list. */ + recur_int_list_dup(&nrpt.bymonth, &(*rpt)->bymonth); + if (!edit_ilist(&nrpt.bymonth, BYMONTH, nrpt.type)) + goto cleanup; + + /* Edit BYMONTHDAY list. */ + if (nrpt.type != RECUR_WEEKLY) { + recur_int_list_dup(&nrpt.bymonthday, &(*rpt)->bymonthday); + if (!edit_ilist(&nrpt.bymonthday, BYMONTHDAY, nrpt.type)) + goto cleanup; + } + + /* The new until may no longer be valid. */ + if (count) { + nrpt.until = 0; + if (!recur_nth_occurrence(start, dur, &nrpt, exc, + count, &until)) { + status_mesg(msg_count, msg_cont); + keys_wgetch(win[KEY].p); + goto cleanup; + } + nrpt.until = DAY(until); + } + /* + * Check whether the start occurrence matches the recurrence rule, in + * other words, does it occur on the start day? This is required by + * RFC5545 and ensures that the recurrence set is non-empty (unless it + * is an exception day). + */ + char *msg_match = + _("Repetition must begin on start day (%s); " + "any change discarded."); + if (!recur_item_find_occurrence(start, dur, &nrpt, NULL, DAY(start), + NULL)) { + mem_free(outstr); + outstr = day_ins(&msg_match, start); + status_mesg(outstr, msg_cont); + keys_wgetch(win[KEY].p); goto cleanup; + } + + /* Update all recurrence parameters. */ + (*rpt)->type = nrpt.type; + (*rpt)->freq = nrpt.freq; + (*rpt)->until = nrpt.until; - (*rpt)->type = recur_char2def(newtype); - (*rpt)->freq = newfreq; - (*rpt)->until = newuntil; + recur_free_exc_list(exc); + recur_exc_dup(exc, &nrpt.exc); + recur_free_int_list(&(*rpt)->bywday); + recur_int_list_dup(&(*rpt)->bywday, &nrpt.bywday); + + recur_free_int_list(&(*rpt)->bymonth); + recur_int_list_dup(&(*rpt)->bymonth, &nrpt.bymonth); + + recur_free_int_list(&(*rpt)->bymonthday); + recur_int_list_dup(&(*rpt)->bymonthday, &nrpt.bymonthday); + + updated = 1; cleanup: - mem_free(msg_rpt_current); - mem_free(msg_rpt_asktype); + mem_free(types); mem_free(freqstr); mem_free(timstr); mem_free(outstr); + recur_free_exc_list(&nrpt.exc); + recur_free_int_list(&nrpt.bywday); + recur_free_int_list(&nrpt.bymonth); + recur_free_int_list(&nrpt.bymonthday); + + return updated; } /* Edit an already existing item. */ +#define ADVANCED 0 void ui_day_item_edit(void) { struct recur_event *re; @@ -482,7 +949,7 @@ void ui_day_item_edit(void) switch (p->type) { case RECUR_EVNT: re = p->item.rev; - const char *choice_recur_evnt[2] = { + const char *choice_recur_evnt[] = { _("Description"), _("Repetition") }; @@ -490,11 +957,9 @@ void ui_day_item_edit(void) (_("Edit: "), choice_recur_evnt, 2)) { case 1: update_desc(&re->mesg); - io_set_modified(); break; case 2: - update_rept(&re->rpt, re->day, &re->exc); - io_set_modified(); + update_rept(re->day, -1, &re->rpt, &re->exc, ADVANCED); break; default: return; @@ -503,7 +968,6 @@ void ui_day_item_edit(void) case EVNT: e = p->item.ev; update_desc(&e->mesg); - io_set_modified(); break; case RECUR_APPT: ra = p->item.rapt; @@ -518,29 +982,25 @@ void ui_day_item_edit(void) (_("Edit: "), choice_recur_appt, 5)) { case 1: need_check_notify = 1; - update_start_time(&ra->start, &ra->dur, ra->dur == 0); - io_set_modified(); + update_start_time(&ra->start, &ra->dur, ra->rpt, ra->dur == 0); break; case 2: update_duration(&ra->start, &ra->dur); - io_set_modified(); break; case 3: if (notify_bar()) need_check_notify = notify_same_recur_item(ra); update_desc(&ra->mesg); - io_set_modified(); break; case 4: need_check_notify = 1; - update_rept(&ra->rpt, ra->start, &ra->exc); - io_set_modified(); + update_rept(ra->start, ra->dur, &ra->rpt, &ra->exc, + ADVANCED); break; case 5: need_check_notify = 1; - update_start_time(&ra->start, &ra->dur, 1); - io_set_modified(); + update_start_time(&ra->start, &ra->dur, ra->rpt, 1); break; default: return; @@ -558,24 +1018,20 @@ void ui_day_item_edit(void) (_("Edit: "), choice_appt, 4)) { case 1: need_check_notify = 1; - update_start_time(&a->start, &a->dur, a->dur == 0); - io_set_modified(); + update_start_time(&a->start, &a->dur, NULL, a->dur == 0); break; case 2: update_duration(&a->start, &a->dur); - io_set_modified(); break; case 3: if (notify_bar()) need_check_notify = notify_same_item(a->start); update_desc(&a->mesg); - io_set_modified(); break; case 4: need_check_notify = 1; - update_start_time(&a->start, &a->dur, 1); - io_set_modified(); + update_start_time(&a->start, &a->dur, NULL, 1); break; default: return; @@ -584,12 +1040,13 @@ void ui_day_item_edit(void) default: break; } - + io_set_modified(); ui_calendar_monthly_view_cache_set_invalid(); if (need_check_notify) notify_check_next_app(1); } +#undef ADVANCED /* Pipe an appointment or event to an external program. */ void ui_day_item_pipe(void) @@ -610,7 +1067,7 @@ void ui_day_item_pipe(void) return; wins_prepare_external(); - if ((pid = shell_exec(NULL, &pout, *arg, arg))) { + if ((pid = shell_exec(NULL, &pout, NULL, 0, *arg, arg))) { fpout = fdopen(pout, "w"); switch (p->type) { @@ -631,7 +1088,7 @@ void ui_day_item_pipe(void) } fclose(fpout); - child_wait(NULL, &pout, pid); + child_wait(NULL, &pout, NULL, pid); press_any_key(); } wins_unprepare_external(); @@ -767,77 +1224,85 @@ void ui_day_item_add(void) /* Delete an item from the appointment list. */ void ui_day_item_delete(unsigned reg) { - const char *del_app_str = - _("Do you really want to delete this item?"); - - const char *erase_warning = - _("This item is recurrent. " - "Delete (a)ll occurences or just this (o)ne?"); - const char *erase_choices = _("[ao]"); - const int nb_erase_choices = 2; - - const char *note_warning = - _("This item has a note attached to it. " - "Delete (i)tem or just its (n)ote?"); - const char *note_choices = _("[in]"); - const int nb_note_choices = 2; + const char *msg, *choices; + int nb_choices; + time_t occurrence; if (day_item_count(0) <= 0) return; struct day_item *p = ui_day_get_sel(); - - if (conf.confirm_delete) { - if (status_ask_bool(del_app_str) != 1) { - wins_erase_status_bar(); - return; - } + int has_note = (day_item_get_note(p) != NULL); + int is_recur = (p->type == RECUR_EVNT || p->type == RECUR_APPT); + + if (has_note && is_recur) { + msg = _("This item is recurrent and has a note attached to it. " + "Delete (s)elected occurrence, (a)ll occurrences, " + "or just its (n)ote?"); + choices = _("[san]"); + nb_choices = 3; + } else if (has_note) { + msg = _("This item has a note attached to it. " + "Delete (s)elected occurrence or just its (n)ote?"); + choices = _("[sn]"); + nb_choices = 2; + } else if (is_recur) { + msg = _("This item is recurrent. " + "Delete (s)elected occurrence or (a)ll occurrences?"); + choices = _("[sa]"); + nb_choices = 2; + } else { + msg = _("Confirm deletion. " + "Delete (s)elected occurrence? Press (s) to confirm."); + choices = _("[s]"); + nb_choices = 1; } - if (day_item_get_note(p)) { - switch (status_ask_choice - (note_warning, note_choices, nb_note_choices)) { - case 1: - break; - case 2: - day_item_erase_note(p); - io_set_modified(); - return; - default: /* User escaped */ - return; - } + int answer = 1; + if (nb_choices > 1 || conf.confirm_delete) { + answer = status_ask_choice(msg, choices, nb_choices); } - if (p->type == RECUR_EVNT || p->type == RECUR_APPT) { - switch (status_ask_choice - (erase_warning, erase_choices, nb_erase_choices)) { - case 1: - break; - case 2: - if (p->type == RECUR_EVNT) { - day_item_add_exc(p, ui_day_sel_date()); - } else { - recur_apoint_find_occurrence(p->item.rapt, - ui_day_sel_date(), - &occurrence); - day_item_add_exc(p, occurrence); - } + /* Always map "all occurrences" to 2 and "note" to 3. */ + if (has_note && !is_recur && answer == 2) + answer = 3; + /* + * The option "selected occurrence" should be treated like "all + * occurrences" for a non-recurrent item (delete the whole item). + */ + if (!is_recur && answer == 1) + answer = 2; - io_set_modified(); - ui_calendar_monthly_view_cache_set_invalid(); - /* Keep the selection on the same day. */ - day_set_sel_data( - day_get_item(listbox_get_sel(&lb_apt) - 1) - ); - return; - default: - return; + switch (answer) { + case 1: + /* Delete selected occurrence (of a recurrent item) only. */ + if (p->type == RECUR_EVNT) { + day_item_add_exc(p, ui_day_sel_date()); + } else { + recur_apoint_find_occurrence(p->item.rapt, + ui_day_sel_date(), + &occurrence); + day_item_add_exc(p, occurrence); } + /* Keep the selection on the same day. */ + day_set_sel_data(day_get_item(listbox_get_sel(&lb_apt) - 1)); + break; + case 2: + /* Delete all occurrences (or a non-recurrent item). */ + ui_day_item_cut(reg); + /* Keep the selection on the same day. */ + day_set_sel_data(day_get_item(listbox_get_sel(&lb_apt) - 1)); + break; + case 3: + /* Delete note. */ + day_item_erase_note(p); + break; + default: + /* User escaped, do nothing. */ + return; } - ui_day_item_cut(reg); - /* Keep the selection on the same day. */ - day_set_sel_data(day_get_item(listbox_get_sel(&lb_apt) - 1)); + io_set_modified(); ui_calendar_monthly_view_cache_set_invalid(); } @@ -851,154 +1316,69 @@ void ui_day_item_delete(unsigned reg) */ void ui_day_item_repeat(void) { - char user_input[BUFSIZ] = ""; - const char *msg_rpt_prefix = _("Enter the repetition type:"); - const char *msg_rpt_daily = _("(d)aily"); - const char *msg_rpt_weekly = _("(w)eekly"); - const char *msg_rpt_monthly = _("(m)onthly"); - const char *msg_rpt_yearly = _("(y)early"); - const char *msg_type_choice = _("[dwmy]"); - const char *mesg_freq_1 = _("Enter the repetition frequency:"); - const char *mesg_wrong_freq = _("Invalid frequency."); - const char *mesg_until_1 = _("Enter end date or duration ('?' for input formats):"); - const char *mesg_help_1 = _("Date: %s (year or month may be omitted). Endless duration: '0'."); - const char *mesg_help_2 = _("Duration in days: +dd. Duration in weeks and days: +??w??d."); - const char *mesg_wrong_1 = _("Invalid date."); - const char *mesg_wrong_2 = _("Press [ENTER] to continue."); - const char *wrong_type_1 = _("This item is already a repeated one."); - const char *wrong_type_2 = _("Press [ENTER] to continue."); - const char *mesg_older = _("Invalid date: end date must come after start date (%s)."); - - char *msg_asktype; - asprintf(&msg_asktype, "%s %s, %s, %s, %s", msg_rpt_prefix, - msg_rpt_daily, msg_rpt_weekly, msg_rpt_monthly, - msg_rpt_yearly); - - int type = 0, freq = 0; - int item_nb; + int item_nb, simple; struct day_item *p; - struct recur_apoint *ra; - time_t until; - unsigned days; + long dur; + struct rpt rpt, *r; + const char *already = _("Already repeated."); + const char *cont = _("Press any key to continue."); + const char *repetition = _("A (s)imple or (a)dvanced repetition?"); + const char *sa = _("[sa]"); if (day_item_count(0) <= 0) - goto cleanup; + return; item_nb = listbox_get_sel(&lb_apt); p = day_get_item(item_nb); if (p->type != APPT && p->type != EVNT) { - status_mesg(wrong_type_1, wrong_type_2); + status_mesg(already, cont); keys_wait_for_any_key(win[KEY].p); - goto cleanup; + return; } - switch (status_ask_choice(msg_asktype, msg_type_choice, 4)) { + switch (status_ask_choice(repetition, sa, 2)) { case 1: - type = RECUR_DAILY; + simple = 1; break; case 2: - type = RECUR_WEEKLY; - break; - case 3: - type = RECUR_MONTHLY; - break; - case 4: - type = RECUR_YEARLY; + simple = 0; break; default: - goto cleanup; - } - - while (freq == 0) { - status_mesg(mesg_freq_1, ""); - if (getstring(win[STA].p, user_input, BUFSIZ, 0, 1) != - GETSTRING_VALID) - goto cleanup; - freq = atoi(user_input); - if (freq == 0) { - status_mesg(mesg_wrong_freq, wrong_type_2); - keys_wait_for_any_key(win[KEY].p); - } - user_input[0] = '\0'; + return; } - char *outstr, *datestr; - for (;;) { - status_mesg(mesg_until_1, ""); - if (getstring(win[STA].p, user_input, BUFSIZ, 0, 1) == GETSTRING_ESC) - goto cleanup; - if (strcmp(user_input, "") == 0 || strcmp(user_input, "0") == 0) { - until = 0; - break; - } - if (*user_input == '?') { - user_input[0] = '\0'; - asprintf(&outstr, mesg_help_1, DATEFMT_DESC(conf.input_datefmt)); - status_mesg(outstr, mesg_help_2); - mem_free(outstr); - wgetch(win[KEY].p); - continue; - } - if (*user_input == '+') { - if (!parse_date_duration(user_input + 1, &days, p->start)) { - status_mesg(mesg_wrong_1, mesg_wrong_2); - keys_wgetch(win[KEY].p); - continue; - } - /* Until is midnight of the day. */ - until = date_sec_change( - update_time_in_date(p->start, 0, 0), - 0, days - ); - } else { - int year, month, day; - if (!parse_date(user_input, conf.input_datefmt, - &year, &month, &day, ui_calendar_get_slctd_day())) { - status_mesg(mesg_wrong_1, mesg_wrong_2); - keys_wgetch(win[KEY].p); - continue; - } - struct date d = { day, month, year }; - until = date2sec(d, 0, 0); - } - /* Compare days (midnights) - until-day may equal start day. */ - if (until >= get_slctd_day()) - break; - - datestr = date_sec2date_str(p->start, DATEFMT(conf.input_datefmt)); - asprintf(&outstr, mesg_older, datestr); - status_mesg(outstr, wrong_type_2); - mem_free(datestr); - mem_free(outstr); - keys_wgetch(win[KEY].p); - } + if (p->type == APPT) + dur = p->item.apt->dur; + else + dur = -1; + rpt.type = -1; + rpt.freq = 1; + rpt.until = 0; + LLIST_INIT(&rpt.bymonth); + LLIST_INIT(&rpt.bywday); + LLIST_INIT(&rpt.bymonthday); + LLIST_INIT(&rpt.exc); + r = &rpt; + if (!update_rept(p->start, dur, &r, &rpt.exc, simple)) + return; - /* Set the selected APP item. */ struct day_item d = empty_day; if (p->type == EVNT) { struct event *ev = p->item.ev; d.item.rev = recur_event_new(ev->mesg, ev->note, ev->day, - ev->id, type, freq, until, NULL); - } else if (p->type == APPT) { + ev->id, &rpt); + } else { struct apoint *apt = p->item.apt; - d.item.rapt = ra = recur_apoint_new(apt->mesg, apt->note, + d.item.rapt = recur_apoint_new(apt->mesg, apt->note, apt->start, apt->dur, - apt->state, type, freq, - until, NULL); + apt->state, &rpt); if (notify_bar()) - notify_check_repeated(ra); - } else { - EXIT(_("wrong item type")); - /* NOTREACHED */ + notify_check_repeated(d.item.rapt); } - day_set_sel_data(&d); ui_day_item_cut(REG_BLACK_HOLE); + day_set_sel_data(&d); io_set_modified(); - ui_calendar_monthly_view_cache_set_invalid(); - -cleanup: - mem_free(msg_asktype); } /* Delete an item and save it in a register. */ @@ -1140,7 +1520,7 @@ void ui_day_draw(int n, WINDOW *win, int y, int hilt, void *cb_data) { struct day_item *item = day_get_item(n); /* The item order always indicates the date. */ - time_t date = update_time_in_date(item->order, 0, 0); + time_t date = DAY(item->order); int width = lb_apt.sw.w - 2, is_slctd; hilt = hilt && (wins_slctd() == APP); diff --git a/src/ui-todo.c b/src/ui-todo.c index b546720..46933b3 100644 --- a/src/ui-todo.c +++ b/src/ui-todo.c @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -158,11 +158,11 @@ void ui_todo_pipe(void) return; wins_prepare_external(); - if ((pid = shell_exec(NULL, &pout, *arg, arg))) { + if ((pid = shell_exec(NULL, &pout, NULL, 0, *arg, arg))) { fpout = fdopen(pout, "w"); todo_write(item, fpout); fclose(fpout); - child_wait(NULL, &pout, pid); + child_wait(NULL, &pout, NULL, pid); press_any_key(); } wins_unprepare_external(); @@ -207,14 +207,16 @@ void ui_todo_draw(int n, WINDOW *win, int y, int hilt, void *cb_data) if (hilt) custom_apply_attr(win, ATTR_HIGHEST); - if (utf8_strwidth(todo->mesg) < width) { - mesg = todo->mesg; - } else { + mesg = todo->mesg; + if (mesg[0] == '\0') + mesg = EMPTY_EVENT_DESC_DEFAULT; + + if (utf8_strwidth(mesg) >= width) { width -= 3; - for (j = 0; todo->mesg[j] && width > 0; j++) { - if (!UTF8_ISCONT(todo->mesg[j])) - width -= utf8_width(&todo->mesg[j]); - buf[j] = todo->mesg[j]; + for (j = 0; mesg[j] && width > 0; j++) { + if (!UTF8_ISCONT(mesg[j])) + width -= utf8_width(&mesg[j]); + buf[j] = mesg[j]; } if (j) { buf[j - 1] = '.'; @@ -314,7 +316,32 @@ void ui_todo_popup_item(void) if (!item) return; - item_in_popup(NULL, NULL, item->mesg, _("TODO:")); + if (item->note) { + /* Assign a sane default note size that will cleanly + * truncate long notes */ + const char *note_heading = _("Note:"); + size_t note_size = 3500; + char note[note_size]; + char *notepath, *msg; + FILE *fp; + + asprintf(¬epath, "%s%s", path_notes, item->note); + fp = fopen(notepath, "r"); + if (fp == NULL) { + item_in_popup(NULL, NULL, item->mesg, _("TODO:")); + return; + } + + note_read_contents(note, note_size, fp); + fclose(fp); + mem_free(notepath); + + asprintf(&msg, "%s\n\n%s\n%s", item->mesg, note_heading, note); + item_in_popup(NULL, NULL, msg, _("TODO:")); + mem_free(msg); + } else { + item_in_popup(NULL, NULL, item->mesg, _("TODO:")); + } } void ui_todo_flag(void) @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/utils.c b/src/utils.c index c19c800..2d30bfc 100644 --- a/src/utils.c +++ b/src/utils.c @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -235,6 +235,8 @@ int status_ask_choice(const char *message, const char choice[], return i + 1; if (ch == ESCAPE) return (-1); + if (ch == RETURN) + return (-2); if (resize) { resize = 0; wins_reset(); @@ -421,22 +423,25 @@ struct date sec2date(time_t t) return d; } -time_t utcdate2sec(struct date day, unsigned hour, unsigned min) +time_t tzdate2sec(struct date day, unsigned hour, unsigned min, char *tznew) { - char *tz; + char *tzold; time_t t; - tz = getenv("TZ"); - if (tz) - tz = mem_strdup(tz); - setenv("TZ", "", 1); + if (!tznew) + return date2sec(day, hour, min); + + tzold = getenv("TZ"); + if (tzold) + tzold = mem_strdup(tzold); + setenv("TZ", tznew, 1); tzset(); t = date2sec(day, hour, min); - if (tz) { - setenv("TZ", tz, 1); - mem_free(tz); + if (tzold) { + setenv("TZ", tzold, 1); + mem_free(tzold); } else { unsetenv("TZ"); } @@ -621,6 +626,42 @@ long min2sec(unsigned minutes) return minutes * MININSEC; } +int modify_wday(int wday, int shift) +{ + return (WEEKINDAYS + wday + shift) % WEEKINDAYS; +} + +/* returns char* representing a wday, used for internal functions */ +char *get_wday_default_string(int wday) +{ + switch(wday) { + case MONDAY: + return "Monday"; + break; + case TUESDAY: + return "Tuesday"; + break; + case WEDNESDAY: + return "Wednesday"; + break; + case THURSDAY: + return "Thursday"; + break; + case FRIDAY: + return "Friday"; + break; + case SATURDAY: + return "Saturday"; + break; + case SUNDAY: + return "Sunday"; + break; + default: + return "Sunday"; + break; + } +} + /* * Display a scroll bar when there are so many items that they * can not be displayed inside the corresponding panel. @@ -985,11 +1026,11 @@ parse_date_interactive(const char *datestr, int *year, int *month, int *day) } /* - * Convert a date duration string into a number of days. + * Convert a date increment string into a number of days. * If start is non-zero, the final end time is validated. * * Allowed formats in lenient BNF: - * <duration> ::= <days> | <period> + * <increment>::= <days> | <period> * <period> ::= [ <weeks>w ][ <days>d ] * Notes: * <days> and <weeks> are any integer >= 0. @@ -997,7 +1038,7 @@ parse_date_interactive(const char *datestr, int *year, int *month, int *day) * * Returns 1 on success and 0 on failure. */ -int parse_date_duration(const char *string, unsigned *days, time_t start) +int parse_date_increment(const char *string, unsigned *days, time_t start) { enum { STATE_INITIAL, @@ -1007,7 +1048,7 @@ int parse_date_duration(const char *string, unsigned *days, time_t start) const char *p; unsigned in = 0, frac = 0, denom = 1; - unsigned dur = 0; + unsigned incr = 0; if (!string || *string == '\0') return 0; @@ -1028,10 +1069,10 @@ int parse_date_duration(const char *string, unsigned *days, time_t start) switch (state) { case STATE_INITIAL: if (*p == 'w') { - dur += in * WEEKINDAYS / denom; + incr += in * WEEKINDAYS / denom; state = STATE_WWDD_DD; } else if (*p == 'd') { - dur += in / denom; + incr += in / denom; state = STATE_DONE; } else { return 0; @@ -1039,7 +1080,7 @@ int parse_date_duration(const char *string, unsigned *days, time_t start) break; case STATE_WWDD_DD: if (*p == 'd') { - dur += in / denom; + incr += in / denom; state = STATE_DONE; } else { return 0; @@ -1055,18 +1096,18 @@ int parse_date_duration(const char *string, unsigned *days, time_t start) } if (state == STATE_DONE && in > 0) return 0; - dur += in; + incr += in; if (start) { - /* wanted: start = start + dur * DAYINSEC */ + /* wanted: start = start + incr * DAYINSEC */ long p; - if (overflow_mul(dur, DAYINSEC, &p)) + if (overflow_mul(incr, DAYINSEC, &p)) return 0; if (overflow_add(start, p, &start)) return 0; if (!check_sec(&start)) return 0; } - *days = dur; + *days = incr; return 1; } @@ -1312,21 +1353,32 @@ void psleep(unsigned secs) /* * Fork and execute an external process. * - * If pfdin and/or pfdout point to a valid address, a pipe is created and the - * appropriate file descriptors are written to pfdin/pfdout. + * If pfdin/pfdout/pfderr point to a valid address, a pipe is created and the + * appropriate file descriptors are written to pfdin/pfdout/pfderr. + * + * If new_session is non-zero, setsid() is called after forking. */ -int fork_exec(int *pfdin, int *pfdout, const char *path, - const char *const *arg) +int fork_exec(int *pfdin, int *pfdout, int *pfderr, int new_session, + const char *path, const char *const *arg) { - int pin[2], pout[2]; + int pin[2], pout[2], perr[2]; int pid; if (pfdin && (pipe(pin) == -1)) return 0; if (pfdout && (pipe(pout) == -1)) return 0; + if (pfderr && (pipe(perr) == -1)) + return 0; if ((pid = fork()) == 0) { + if (pfderr) { + if (dup2(perr[0], STDERR_FILENO) < 0) + _exit(127); + close(perr[0]); + close(perr[1]); + } + if (pfdout) { if (dup2(pout[0], STDIN_FILENO) < 0) _exit(127); @@ -1341,6 +1393,11 @@ int fork_exec(int *pfdin, int *pfdout, const char *path, close(pin[1]); } + if (new_session) { + if ((setsid() < 0)) + _exit(127); + } + execvp(path, (char *const *)arg); _exit(127); } else { @@ -1348,6 +1405,8 @@ int fork_exec(int *pfdin, int *pfdout, const char *path, close(pin[1]); if (pfdout) close(pout[0]); + if (pfderr) + close(perr[0]); if (pid > 0) { if (pfdin) { @@ -1358,11 +1417,17 @@ int fork_exec(int *pfdin, int *pfdout, const char *path, fcntl(pout[1], F_SETFD, FD_CLOEXEC); *pfdout = pout[1]; } + if (pfderr) { + fcntl(perr[1], F_SETFD, FD_CLOEXEC); + *pfderr = perr[1]; + } } else { if (pfdin) close(pin[0]); if (pfdout) close(pout[1]); + if (pfderr) + close(perr[1]); return 0; } } @@ -1371,8 +1436,8 @@ int fork_exec(int *pfdin, int *pfdout, const char *path, /* Execute an external program in a shell. */ int -shell_exec(int *pfdin, int *pfdout, const char *path, - const char *const *arg) +shell_exec(int *pfdin, int *pfdout, int *pfderr, int new_session, + const char *path, const char *const *arg) { int argc, i; const char **narg; @@ -1401,7 +1466,7 @@ shell_exec(int *pfdin, int *pfdout, const char *path, narg[3] = NULL; } - ret = fork_exec(pfdin, pfdout, *narg, narg); + ret = fork_exec(pfdin, pfdout, pfderr, new_session, *narg, narg); if (arg0) mem_free(arg0); @@ -1411,7 +1476,7 @@ shell_exec(int *pfdin, int *pfdout, const char *path, } /* Wait for a child process to terminate. */ -int child_wait(int *pfdin, int *pfdout, int pid) +int child_wait(int *pfdin, int *pfdout, int *pfderr, int pid) { int stat; @@ -1419,9 +1484,13 @@ int child_wait(int *pfdin, int *pfdout, int pid) close(*pfdin); if (pfdout) close(*pfdout); + if (pfderr) + close(*pfderr); - waitpid(pid, &stat, 0); - return stat; + if (waitpid(pid, &stat, 0) == pid) + return stat; + else + return -1; } /* Display "Press any key to continue..." and wait for a key press. */ @@ -1636,7 +1705,7 @@ static void print_date(time_t date, time_t day, const char *extformat) if (!strcmp(extformat, "epoch")) { printf("%ld", (long)date); } else { - time_t day_start = update_time_in_date(day, 0, 0); + time_t day_start = DAY(day); time_t day_end = date_sec_change(day_start, 0, 1); struct tm lt; @@ -1971,11 +2040,6 @@ int hash_matches(const char *pattern, const char *hash) return (starts_with(hash, pattern) != invert); } -int show_dialogs(void) -{ - return (!quiet) && conf.system_dialogs; -} - /* * Overflow check for addition with positive second term. */ @@ -2009,3 +2073,76 @@ long overflow_mul(long x, long y, long *z) *z = x * y; return 0; } + +/* + * Return the upcoming weekday from day (possibly day itself). + */ +time_t next_wday(time_t day, int weekday) +{ + struct tm tm; + + localtime_r(&day, &tm); + return date_sec_change( + day, 0, (weekday - tm.tm_wday + WEEKINDAYS) % WEEKINDAYS + ); + +} + +/* + * Return the number of weekdays of the year. + */ +int wday_per_year(int year, int weekday) +{ + struct tm y_end; + struct date day; + int last_wday; + + /* Find weekday and yearday of the last day of the year. */ + day.dd = 31; + day.mm = 12; + day.yyyy = year; + y_end = date2tm(day, 0, 0); + mktime(&y_end); + + /* Find date of the last weekday of the year. */ + last_wday = (y_end.tm_yday + 1) - (y_end.tm_wday - weekday + 7) % 7; + + return last_wday / 7 + (last_wday % 7 > 0); +} + +/* + * Return the number of weekdays in month of year. + */ +int wday_per_month(int month, int year, int weekday) +{ + struct tm m_end; + struct date day; + int last_wday, m_days = days[month - 1] + (month == 2 && ISLEAP(year) ? 1 : 0); + + /* Find weekday of the last day of the month. */ + day.dd = m_days; + day.mm = month; + day.yyyy = year; + m_end = date2tm(day, 0, 0); + mktime(&m_end); + + /* Find date of the last weekday of the month. */ + last_wday = m_days - (m_end.tm_wday - weekday + 7) % 7; + + return last_wday / 7 + (last_wday % 7 > 0); +} + +/* + * Return allocated string with day of 't' inserted in 'template' in the user's + * preferred format; template must be a "printf" template with exactly one + * string conversion (%s). + */ +char *day_ins(char **template, time_t t) +{ + char *day, *msg; + + day = date_sec2date_str(DAY(t), DATEFMT(conf.input_datefmt)); + asprintf(&msg, *template, day); + mem_free(day); + return msg; +} @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -61,7 +61,7 @@ enum ui_mode ui_mode = UI_CMDLINE; /* Don't save anything if this is set. */ int read_only = 0; -/* Hide system dialogs if set. */ +/* Hide import/export message if set. */ int quiet = 0; /* Applications can trigger a reload by sending SIGUSR1. */ @@ -136,11 +136,11 @@ void vars_init(void) conf.systemevents = 1; conf.default_panel = CAL; conf.compact_panels = 0; - conf.system_dialogs = 1; strncpy(conf.output_datefmt, "%D", 3); conf.input_datefmt = 1; conf.heading_pos = RIGHT; strcpy(conf.day_heading, DAY_HEADING_DEFAULT); + strcpy(conf.timefmt, APPT_TIME_DEFAULT); datefmt_str[0] = _("mm/dd/yyyy"); datefmt_str[1] = _("dd/mm/yyyy"); @@ -175,6 +175,8 @@ void vars_init(void) ui_calendar_set_first_day_of_week(MONDAY); + wins_set_sbar_width(col * SBARMINWIDTH / 100); + /* Pad structure to scroll text inside the appointment panel */ apad.length = 1; apad.first_onscreen = 0; diff --git a/src/vector.c b/src/vector.c index fd468ab..796ef8e 100644 --- a/src/vector.c +++ b/src/vector.c @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/src/vector.h b/src/vector.h index 86cb3b8..7648df2 100644 --- a/src/vector.h +++ b/src/vector.h @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -1,7 +1,7 @@ /* * Calcurse - text-based organizer * - * Copyright (c) 2004-2020 calcurse Development Team <misc@calcurse.org> + * Copyright (c) 2004-2023 calcurse Development Team <misc@calcurse.org> * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -63,7 +63,7 @@ static int layout; /* * The screen_mutex mutex and wins_refresh(), wins_wrefresh(), wins_doupdate() * functions are used to prevent concurrent updates of the screen. - * It was observed that the display could get screwed up when mulitple threads + * It was observed that the display could get screwed up when multiple threads * tried to refresh the screen at the same time. * * Note (2010-03-21): @@ -229,7 +229,7 @@ void wins_sbar_winc(void) void wins_sbar_wdec(void) { - if (sbarwidth_perc > 0) + if (col * sbarwidth_perc / 100 > SBARMINWIDTH) sbarwidth_perc--; } @@ -254,6 +254,15 @@ void wins_slctd_next(void) slctd_win++; } +/* Shift-TAB key was hit in the interface, need to select previous window. */ +void wins_slctd_prev(void) +{ + if (slctd_win == CAL) + slctd_win = TOD; + else + slctd_win--; +} + static void wins_init_panels(void) { wins_scrollwin_init(&sw_cal, win[CAL].y, win[CAL].x, @@ -593,8 +602,6 @@ void wins_prepare_external(void) { if (notify_bar()) notify_stop_main_thread(); - if (conf.periodic_save > 0) - io_stop_psave_thread(); def_prog_mode(); ui_mode = UI_CMDLINE; clear(); @@ -615,8 +622,6 @@ void wins_unprepare_external(void) wins_resize(); if (notify_bar()) notify_start_main_thread(); - if (conf.periodic_save > 0) - io_start_psave_thread(); } /* @@ -628,8 +633,8 @@ void wins_launch_external(const char *arg[]) int pid; wins_prepare_external(); - if ((pid = shell_exec(NULL, NULL, *arg, arg))) - child_wait(NULL, NULL, pid); + if ((pid = shell_exec(NULL, NULL, NULL, 0, *arg, arg))) + child_wait(NULL, NULL, NULL, pid); wins_unprepare_external(); } @@ -655,6 +660,7 @@ void wins_update_bindings(void) static int bindings_cal[] = { KEY_GENERIC_HELP, KEY_GENERIC_QUIT, KEY_GENERIC_SAVE, KEY_GENERIC_RELOAD, KEY_GENERIC_CHANGE_VIEW, + KEY_GENERIC_PREV_VIEW, KEY_GENERIC_SCROLL_DOWN, KEY_GENERIC_SCROLL_UP, KEY_MOVE_UP, KEY_MOVE_DOWN, KEY_MOVE_LEFT, KEY_MOVE_RIGHT, KEY_GENERIC_GOTO, KEY_GENERIC_IMPORT, KEY_GENERIC_EXPORT, KEY_START_OF_WEEK, @@ -670,6 +676,7 @@ void wins_update_bindings(void) static int bindings_apoint[] = { KEY_GENERIC_HELP, KEY_GENERIC_QUIT, KEY_GENERIC_SAVE, KEY_GENERIC_RELOAD, KEY_GENERIC_CHANGE_VIEW, + KEY_GENERIC_PREV_VIEW, KEY_GENERIC_IMPORT, KEY_GENERIC_EXPORT, KEY_ADD_ITEM, KEY_DEL_ITEM, KEY_EDIT_ITEM, KEY_VIEW_ITEM, KEY_PIPE_ITEM, KEY_GENERIC_REDRAW, KEY_REPEAT_ITEM, KEY_FLAG_ITEM, @@ -686,6 +693,7 @@ void wins_update_bindings(void) static int bindings_todo[] = { KEY_GENERIC_HELP, KEY_GENERIC_QUIT, KEY_GENERIC_SAVE, KEY_GENERIC_RELOAD, KEY_GENERIC_CHANGE_VIEW, + KEY_GENERIC_PREV_VIEW, KEY_GENERIC_SCROLL_DOWN, KEY_GENERIC_SCROLL_UP, KEY_GENERIC_IMPORT, KEY_GENERIC_EXPORT, KEY_ADD_ITEM, KEY_DEL_ITEM, KEY_EDIT_ITEM, KEY_VIEW_ITEM, KEY_PIPE_ITEM, |