/* * Calcurse - text-based organizer * * Copyright (c) 2004-2023 calcurse Development Team * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * - Redistributions of source code must retain the above * copyright notice, this list of conditions and the * following disclaimer. * * - Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the * following disclaimer in the documentation and/or other * materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * Send your feedback or comments to : misc@calcurse.org * Calcurse home page : http://calcurse.org * */ #include #include #include #include #include #include "calcurse.h" #define NOTIFY_FIELD_LENGTH 25 struct notify_vars { WINDOW *win; char *apts_file; char time[NOTIFY_FIELD_LENGTH]; char date[NOTIFY_FIELD_LENGTH]; pthread_mutex_t mutex; }; static struct notify_vars notify; static struct notify_app notify_app; static pthread_attr_t detached_thread_attr; /* * Return the number of seconds before next appointment * (0 if no upcoming appointment). */ int notify_time_left(void) { time_t ntimer; int left; ntimer = time(NULL); left = notify_app.time - ntimer; return left > 0 ? left : 0; } static unsigned notify_trigger(void) { int flagged = notify_app.state & APOINT_NOTIFY; if (!notify_app.got_app) return 0; if (nbar.notify_all == NOTIFY_ALL) return 1; if (nbar.notify_all == NOTIFY_UNFLAGGED_ONLY) flagged = !flagged; return flagged; } /* * Return 1 if the reminder was not sent already for the upcoming appointment. */ unsigned notify_needs_reminder(void) { if (notify_app.state & APOINT_NOTIFIED) return 0; return notify_trigger(); } /* * This is used to update the notify_app structure. * Note: the mutex associated with this structure must be locked by the * caller! */ void notify_update_app(time_t start, char state, char *msg) { notify_free_app(); notify_app.got_app = 1; notify_app.time = start; notify_app.state = state; notify_app.txt = mem_strdup(msg); } /* Return 1 if we need to display the notify-bar, else 0. */ int notify_bar(void) { int display_bar = 0; pthread_mutex_lock(&nbar.mutex); display_bar = (nbar.show) ? 1 : 0; pthread_mutex_unlock(&nbar.mutex); return display_bar; } /* Initialize the nbar variable used to store notification options. */ void notify_init_vars(void) { const char *time_format = "%T"; const char *date_format = "%a %F"; const char *cmd = "printf '\\a'"; pthread_mutex_init(&nbar.mutex, NULL); nbar.show = 1; nbar.cntdwn = 300; strncpy(nbar.datefmt, date_format, BUFSIZ); nbar.datefmt[BUFSIZ - 1] = '\0'; strncpy(nbar.timefmt, time_format, BUFSIZ); nbar.timefmt[BUFSIZ - 1] = '\0'; strncpy(nbar.cmd, cmd, BUFSIZ); nbar.cmd[BUFSIZ - 1] = '\0'; nbar.notify_all = 0; pthread_attr_init(&detached_thread_attr); pthread_attr_setdetachstate(&detached_thread_attr, PTHREAD_CREATE_DETACHED); } /* Extract the appointment file name from the complete file path. */ static void extract_aptsfile(void) { char *file; file = strrchr(path_apts, '/'); if (!file) { notify.apts_file = path_apts; } else { notify.apts_file = file; notify.apts_file++; } } /* * Create the notification bar, by initializing all the variables and * creating the notification window (l is the number of lines, c the * number of columns, y and x are its coordinates). */ void notify_init_bar(void) { pthread_mutex_init(¬ify.mutex, NULL); pthread_mutex_init(¬ify_app.mutex, NULL); notify_app.got_app = 0; notify_app.txt = 0; notify.win = newwin(win[NOT].h, win[NOT].w, win[NOT].y, win[NOT].x); extract_aptsfile(); } /* * Free memory associated with the notify_app structure. */ void notify_free_app(void) { notify_app.time = 0; notify_app.got_app = 0; notify_app.state = APOINT_NULL; if (notify_app.txt) mem_free(notify_app.txt); notify_app.txt = 0; } /* Stop the notify-bar main thread. */ void notify_stop_main_thread(void) { /* Is the thread running? */ if (pthread_equal(notify_t_main, pthread_self())) return; pthread_cancel(notify_t_main); pthread_join(notify_t_main, NULL); notify_t_main = pthread_self(); } /* * The calcurse window geometry has changed so we need to reset the * notification window. */ void notify_reinit_bar(void) { delwin(notify.win); notify.win = newwin(win[NOT].h, win[NOT].w, win[NOT].y, win[NOT].x); } /* Launch user defined command as a notification. */ unsigned notify_launch_cmd(void) { 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; if ((pid = shell_exec(&pin, &pout, &perr, 1, *arg, arg))) { close(pin); close(pout); close(perr); } return 1; } /* * Update the notification bar. This is useful when changing color theme * for example. */ void notify_update_bar(void) { const int space = 3; int file_pos, date_pos, app_pos, txt_max_len; int time_left, blinking; date_pos = space; pthread_mutex_lock(¬ify.mutex); file_pos = strlen(notify.date) + strlen(notify.time) + 7 + 2 * space; app_pos = file_pos + strlen(notify.apts_file) + 2 + space; txt_max_len = MAX(col - (app_pos + 12 + space), 3); WINS_NBAR_LOCK; custom_apply_attr(notify.win, ATTR_HIGHEST); wattron(notify.win, A_UNDERLINE | A_REVERSE); mvwhline(notify.win, 0, 0, ACS_HLINE, col); mvwprintw(notify.win, 0, date_pos, "[ %s | %s ]", notify.date, notify.time); mvwprintw(notify.win, 0, file_pos, "(%s)", notify.apts_file); WINS_NBAR_UNLOCK; pthread_mutex_lock(¬ify_app.mutex); if (notify_app.got_app) { char buf[txt_max_len * UTF8_MAXLEN]; strncpy(buf, notify_app.txt, txt_max_len * UTF8_MAXLEN); buf[sizeof(buf) - 1] = '\0'; utf8_chop(buf, txt_max_len); time_left = notify_time_left(); if (time_left > 0) { int hours_left, minutes_left; /* In minutes rounded up. */ minutes_left = time_left / MININSEC + (time_left % MININSEC ? 1 : 0); hours_left = minutes_left / HOURINMIN; minutes_left = minutes_left % HOURINMIN; pthread_mutex_lock(&nbar.mutex); blinking = time_left <= nbar.cntdwn && notify_trigger(); WINS_NBAR_LOCK; if (blinking) wattron(notify.win, A_BLINK); mvwprintw(notify.win, 0, app_pos, "> %02d:%02d :: %s <", hours_left, minutes_left, buf); if (blinking) wattroff(notify.win, A_BLINK); WINS_NBAR_UNLOCK; if (blinking) notify_launch_cmd(); pthread_mutex_unlock(&nbar.mutex); } else { notify_app.got_app = 0; pthread_mutex_unlock(¬ify_app.mutex); pthread_mutex_unlock(¬ify.mutex); notify_check_next_app(0); return; } } pthread_mutex_unlock(¬ify_app.mutex); WINS_NBAR_LOCK; wattroff(notify.win, A_UNDERLINE | A_REVERSE); custom_remove_attr(notify.win, ATTR_HIGHEST); WINS_NBAR_UNLOCK; wins_wrefresh(notify.win); pthread_mutex_unlock(¬ify.mutex); } static void notify_main_thread_cleanup(void *arg) { pthread_mutex_trylock(¬ify.mutex); pthread_mutex_unlock(¬ify.mutex); pthread_mutex_trylock(&nbar.mutex); pthread_mutex_unlock(&nbar.mutex); } /* Update the notication bar content */ /* ARGSUSED0 */ static void *notify_main_thread(void *arg) { const unsigned thread_sleep = 1; const unsigned check_app = MININSEC; int elapse = 0; int got_app; struct tm ntime; time_t ntimer; elapse = 0; pthread_cleanup_push(notify_main_thread_cleanup, NULL); for (;;) { ntimer = time(NULL); localtime_r(&ntimer, &ntime); pthread_mutex_lock(¬ify.mutex); pthread_mutex_lock(&nbar.mutex); strftime(notify.time, NOTIFY_FIELD_LENGTH, nbar.timefmt, &ntime); strftime(notify.date, NOTIFY_FIELD_LENGTH, nbar.datefmt, &ntime); pthread_mutex_unlock(&nbar.mutex); 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; pthread_mutex_lock(¬ify_app.mutex); got_app = notify_app.got_app; pthread_mutex_unlock(¬ify_app.mutex); if (!got_app) notify_check_next_app(0); } } pthread_cleanup_pop(0); pthread_exit(NULL); } /* Fill the given structure with information about next appointment. */ unsigned notify_get_next(struct notify_app *a) { time_t current_time; if (!a) return 0; current_time = time(NULL); a->time = current_time + DAYINSEC; a->got_app = 0; a->state = 0; a->txt = NULL; recur_apoint_check_next(a, current_time, get_today()); apoint_check_next(a, current_time); return 1; } /* * This is used for the daemon to check if we have an upcoming appointment or * not. */ unsigned notify_get_next_bkgd(void) { struct notify_app a; a.txt = NULL; if (!notify_get_next(&a)) return 0; if (!a.got_app) { /* No next appointment, reset the previous notified one. */ notify_app.got_app = 0; return 1; } else { if (!notify_same_item(a.time)) notify_update_app(a.time, a.state, a.txt); } if (a.txt) mem_free(a.txt); return 1; } /* Return the description of next appointment to be notified. */ char *notify_app_txt(void) { if (notify_app.got_app) return notify_app.txt; else return NULL; } /* Look for the next appointment within the next 24 hours. */ /* ARGSUSED0 */ static void *notify_thread_app(void *arg) { struct notify_app tmp_app; int force = (arg ? 1 : 0); if (!notify_get_next(&tmp_app)) pthread_exit(NULL); if (!tmp_app.got_app) { pthread_mutex_lock(¬ify_app.mutex); notify_free_app(); pthread_mutex_unlock(¬ify_app.mutex); } else { if (force || !notify_same_item(tmp_app.time)) { pthread_mutex_lock(¬ify_app.mutex); notify_update_app(tmp_app.time, tmp_app.state, tmp_app.txt); pthread_mutex_unlock(¬ify_app.mutex); } } if (tmp_app.txt) mem_free(tmp_app.txt); notify_update_bar(); pthread_exit(NULL); } /* Launch the thread notify_thread_app to look for next appointment. */ void notify_check_next_app(int force) { pthread_t notify_t_app; void *arg = (force ? (void *)1 : NULL); pthread_create(¬ify_t_app, &detached_thread_attr, notify_thread_app, arg); return; } /* Check if the newly created appointment is to be notified. */ void notify_check_added(char *mesg, time_t start, char state) { time_t current_time; int update_notify = 0; long gap; current_time = time(NULL); pthread_mutex_lock(¬ify_app.mutex); if (!notify_app.got_app) { gap = start - current_time; if (gap >= 0 && gap <= DAYINSEC) update_notify = 1; } else if (start < notify_app.time && start >= current_time) { update_notify = 1; } else if (start == notify_app.time && state != notify_app.state) { update_notify = 1; } if (update_notify) { notify_update_app(start, state, mesg); } pthread_mutex_unlock(¬ify_app.mutex); notify_update_bar(); } /* Check if the newly repeated appointment is to be notified. */ void notify_check_repeated(struct recur_apoint *i) { time_t current_time, real_app_time; int update_notify = 0; current_time = time(NULL); pthread_mutex_lock(¬ify_app.mutex); if (recur_item_find_occurrence (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; } else if (real_app_time < notify_app.time && real_app_time >= current_time) { update_notify = 1; } else if (real_app_time == notify_app.time && i->state != notify_app.state) { update_notify = 1; } } if (update_notify) { notify_update_app(real_app_time, i->state, i->mesg); } pthread_mutex_unlock(¬ify_app.mutex); notify_update_bar(); } int notify_same_item(time_t time) { int same = 0; pthread_mutex_lock(&(notify_app.mutex)); if (notify_app.got_app && notify_app.time == time) same = 1; pthread_mutex_unlock(&(notify_app.mutex)); return same; } /* * Check if an occurrence of a recurrent appointment is currently the "next * upcoming appointment" in the notify bar. */ int notify_same_recur_item(struct recur_apoint *i) { int same = 0; time_t item_start; /* Tomorrow? */ 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->rpt, &i->exc, get_today(), &item_start); pthread_mutex_lock(¬ify_app.mutex); if (notify_app.got_app && item_start == notify_app.time) same = 1; pthread_mutex_unlock(&(notify_app.mutex)); return same; } /* Launch the notify-bar main thread. */ void notify_start_main_thread(void) { /* Avoid starting the notification bar thread twice. */ notify_stop_main_thread(); pthread_create(¬ify_t_main, NULL, notify_main_thread, NULL); notify_check_next_app(0); } /* * Print an option in the configuration menu. * Specific treatment is needed depending on if the option is of type boolean * (either YES or NO), or an option holding a string value. */ static void print_option(WINDOW * win, unsigned x, unsigned y, char *name, char *valstr, unsigned valbool, char *desc) { const int MAXCOL = col - 3; int x_opt, len; x_opt = x + strlen(name); mvwprintw(win, y, x, "%s", name); erase_window_part(win, x_opt, y, MAXCOL, y); if ((len = strlen(valstr)) != 0) { unsigned maxlen = MAX(MAXCOL - x_opt - 2, 3); char buf[maxlen * UTF8_MAXLEN]; strncpy(buf, valstr, maxlen * UTF8_MAXLEN); buf[maxlen * UTF8_MAXLEN - 1] = '\0'; utf8_chop(buf, maxlen); custom_apply_attr(win, ATTR_HIGHEST); mvwaddstr(win, y, x_opt, buf); custom_remove_attr(win, ATTR_HIGHEST); } else { print_bool_option_incolor(win, valbool, y, x_opt); } mvwaddstr(win, y + 1, x, desc); } /* Print options related to the notify-bar. */ static void print_config_option(int i, WINDOW *win, int y, int hilt, void *cb_data) { enum { SHOW, DATE, CLOCK, WARN, CMD, NOTIFYALL, DMON, DMON_LOG, NB_OPT }; struct opt_s { char *name; char *desc; char valstr[BUFSIZ]; unsigned valnum; } opt[NB_OPT]; opt[SHOW].name = "appearance.notifybar = "; opt[SHOW].desc = _("(if set to YES, notify-bar will be displayed)"); opt[DATE].name = "format.notifydate = "; opt[DATE].desc = _("(Format of the date to be displayed inside notify-bar)"); opt[CLOCK].name = "format.notifytime = "; opt[CLOCK].desc = _("(Format of the time to be displayed inside notify-bar)"); opt[WARN].name = "notification.warning = "; opt[WARN].desc = _("(Warn user if an appointment is within next " "'notify-bar_warning' seconds)"); opt[CMD].name = "notification.command = "; opt[CMD].desc = _("(Command used to notify user of an upcoming appointment)"); opt[NOTIFYALL].name = "notification.notifyall = "; opt[NOTIFYALL].desc = _("(Notify all appointments instead of flagged ones only)"); opt[DMON].name = "daemon.enable = "; opt[DMON].desc = _("(Run in background to get notifications after exiting)"); opt[DMON_LOG].name = "daemon.log = "; opt[DMON_LOG].desc = _("(Log activity when running in background)"); pthread_mutex_lock(&nbar.mutex); /* String value options */ strncpy(opt[DATE].valstr, nbar.datefmt, BUFSIZ); strncpy(opt[CLOCK].valstr, nbar.timefmt, BUFSIZ); snprintf(opt[WARN].valstr, BUFSIZ, "%d", nbar.cntdwn); strncpy(opt[CMD].valstr, nbar.cmd, BUFSIZ); /* Boolean options */ opt[SHOW].valnum = nbar.show; pthread_mutex_unlock(&nbar.mutex); opt[DMON].valnum = dmon.enable; opt[DMON_LOG].valnum = dmon.log; opt[SHOW].valstr[0] = opt[DMON].valstr[0] = opt[DMON_LOG].valstr[0] = '\0'; opt[NOTIFYALL].valnum = nbar.notify_all; if (opt[NOTIFYALL].valnum == NOTIFY_FLAGGED_ONLY) strcpy(opt[NOTIFYALL].valstr, "flagged-only"); else if (opt[NOTIFYALL].valnum == NOTIFY_UNFLAGGED_ONLY) strcpy(opt[NOTIFYALL].valstr, "unflagged-only"); else if (opt[NOTIFYALL].valnum == NOTIFY_ALL) strcpy(opt[NOTIFYALL].valstr, "all"); if (hilt) custom_apply_attr(win, ATTR_HIGHEST); print_option(win, 1, y, opt[i].name, opt[i].valstr, opt[i].valnum, opt[i].desc); if (hilt) custom_remove_attr(win, ATTR_HIGHEST); } static enum listbox_row_type config_option_row_type(int i, void *cb_data) { return LISTBOX_ROW_TEXT; } static int config_option_height(int i, void *cb_data) { return 3; } static void config_option_edit(int i) { char *buf; const char *date_str = _("Enter the date format (see 'man 3 strftime' for possible formats) "); const char *time_str = _("Enter the time format (see 'man 3 strftime' for possible formats) "); const char *count_str = _("Enter the number of seconds (0 not to be warned before an appointment)"); const char *cmd_str = _("Enter the notification command "); buf = mem_malloc(BUFSIZ); buf[0] = '\0'; switch (i) { case 0: pthread_mutex_lock(&nbar.mutex); nbar.show = !nbar.show; pthread_mutex_unlock(&nbar.mutex); if (notify_bar()) notify_start_main_thread(); else notify_stop_main_thread(); resize = 1; break; case 1: status_mesg(date_str, ""); pthread_mutex_lock(&nbar.mutex); strncpy(buf, nbar.datefmt, BUFSIZ); buf[BUFSIZ - 1] = '\0'; pthread_mutex_unlock(&nbar.mutex); if (updatestring(win[STA].p, &buf, 0, 1) == 0) { pthread_mutex_lock(&nbar.mutex); strncpy(nbar.datefmt, buf, BUFSIZ); nbar.datefmt[BUFSIZ - 1] = '\0'; pthread_mutex_unlock(&nbar.mutex); } break; case 2: status_mesg(time_str, ""); pthread_mutex_lock(&nbar.mutex); strncpy(buf, nbar.timefmt, BUFSIZ); buf[BUFSIZ - 1] = '\0'; pthread_mutex_unlock(&nbar.mutex); if (updatestring(win[STA].p, &buf, 0, 1) == 0) { pthread_mutex_lock(&nbar.mutex); strncpy(nbar.timefmt, buf, BUFSIZ); nbar.timefmt[BUFSIZ - 1] = '\0'; pthread_mutex_unlock(&nbar.mutex); } break; case 3: status_mesg(count_str, ""); pthread_mutex_lock(&nbar.mutex); snprintf(buf, BUFSIZ, "%d", nbar.cntdwn); pthread_mutex_unlock(&nbar.mutex); if (updatestring(win[STA].p, &buf, 0, 1) == 0 && is_all_digit(buf) && atoi(buf) >= 0 && atoi(buf) <= DAYINSEC) { pthread_mutex_lock(&nbar.mutex); nbar.cntdwn = atoi(buf); pthread_mutex_unlock(&nbar.mutex); } break; case 4: status_mesg(cmd_str, ""); pthread_mutex_lock(&nbar.mutex); strncpy(buf, nbar.cmd, BUFSIZ); buf[BUFSIZ - 1] = '\0'; pthread_mutex_unlock(&nbar.mutex); if (updatestring(win[STA].p, &buf, 0, 1) == 0) { pthread_mutex_lock(&nbar.mutex); strncpy(nbar.cmd, buf, BUFSIZ); nbar.cmd[BUFSIZ - 1] = '\0'; pthread_mutex_unlock(&nbar.mutex); } break; case 5: pthread_mutex_lock(&nbar.mutex); nbar.notify_all = (nbar.notify_all + 1) % 3; pthread_mutex_unlock(&nbar.mutex); notify_check_next_app(1); break; case 6: dmon.enable = !dmon.enable; break; case 7: dmon.log = !dmon.log; break; } mem_free(buf); } /* Notify-bar configuration. */ void notify_config_bar(void) { static int bindings[] = { KEY_GENERIC_QUIT, KEY_MOVE_UP, KEY_MOVE_DOWN, KEY_EDIT_ITEM }; struct listbox lb; int key; clear(); listbox_init(&lb, 0, 0, notify_bar() ? row - 3 : row - 2, col, _("notification options"), config_option_row_type, config_option_height, print_config_option); listbox_load_items(&lb, 8); listbox_draw_deco(&lb, 0); listbox_display(&lb, NOHILT); wins_set_bindings(bindings, ARRAY_SIZE(bindings)); wins_status_bar(); wnoutrefresh(win[STA].p); wmove(win[STA].p, 0, 0); wins_doupdate(); while ((key = keys_get(win[KEY].p, NULL, NULL)) != KEY_GENERIC_QUIT) { switch (key) { case KEY_MOVE_DOWN: listbox_sel_move(&lb, 1); break; case KEY_MOVE_UP: listbox_sel_move(&lb, -1); break; case KEY_EDIT_ITEM: config_option_edit(listbox_get_sel(&lb)); break; } if (resize) { resize = 0; wins_get_config(); wins_reset_noupdate(); listbox_resize(&lb, 0, 0, notify_bar() ? row - 3 : row - 2, col); listbox_draw_deco(&lb, 0); delwin(win[STA].p); win[STA].p = newwin(win[STA].h, win[STA].w, win[STA].y, win[STA].x); keypad(win[STA].p, TRUE); if (notify_bar()) { notify_reinit_bar(); notify_update_bar(); } clearok(curscr, TRUE); } listbox_display(&lb, NOHILT); wins_status_bar(); wnoutrefresh(win[STA].p); wmove(win[STA].p, 0, 0); wins_doupdate(); } listbox_delete(&lb); }