root/usr.bin/tmux/alerts.c
/* $OpenBSD: alerts.c,v 1.34 2025/02/10 08:14:32 nicm Exp $ */

/*
 * Copyright (c) 2015 Nicholas Marriott <nicholas.marriott@gmail.com>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
 * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
 * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <sys/types.h>

#include <event.h>
#include <stdlib.h>

#include "tmux.h"

static int      alerts_fired;

static void     alerts_timer(int, short, void *);
static int      alerts_enabled(struct window *, int);
static void     alerts_callback(int, short, void *);
static void     alerts_reset(struct window *);

static int      alerts_action_applies(struct winlink *, const char *);
static int      alerts_check_all(struct window *);
static int      alerts_check_bell(struct window *);
static int      alerts_check_activity(struct window *);
static int      alerts_check_silence(struct window *);
static void     alerts_set_message(struct winlink *, const char *,
                    const char *);

static TAILQ_HEAD(, window) alerts_list = TAILQ_HEAD_INITIALIZER(alerts_list);

static void
alerts_timer(__unused int fd, __unused short events, void *arg)
{
        struct window   *w = arg;

        log_debug("@%u alerts timer expired", w->id);
        alerts_queue(w, WINDOW_SILENCE);
}

static void
alerts_callback(__unused int fd, __unused short events, __unused void *arg)
{
        struct window   *w, *w1;
        int              alerts;

        TAILQ_FOREACH_SAFE(w, &alerts_list, alerts_entry, w1) {
                alerts = alerts_check_all(w);
                log_debug("@%u alerts check, alerts %#x", w->id, alerts);

                w->alerts_queued = 0;
                TAILQ_REMOVE(&alerts_list, w, alerts_entry);

                w->flags &= ~WINDOW_ALERTFLAGS;
                window_remove_ref(w, __func__);
        }
        alerts_fired = 0;
}

static int
alerts_action_applies(struct winlink *wl, const char *name)
{
        int     action;

        /*
         * {bell,activity,silence}-action determines when to alert: none means
         * nothing happens, current means only do something for the current
         * window and other means only for windows other than the current.
         */

        action = options_get_number(wl->session->options, name);
        if (action == ALERT_ANY)
                return (1);
        if (action == ALERT_CURRENT)
                return (wl == wl->session->curw);
        if (action == ALERT_OTHER)
                return (wl != wl->session->curw);
        return (0);
}

static int
alerts_check_all(struct window *w)
{
        int     alerts;

        alerts  = alerts_check_bell(w);
        alerts |= alerts_check_activity(w);
        alerts |= alerts_check_silence(w);
        return (alerts);
}

void
alerts_check_session(struct session *s)
{
        struct winlink  *wl;

        RB_FOREACH(wl, winlinks, &s->windows)
                alerts_check_all(wl->window);
}

static int
alerts_enabled(struct window *w, int flags)
{
        if (flags & WINDOW_BELL) {
                if (options_get_number(w->options, "monitor-bell"))
                        return (1);
        }
        if (flags & WINDOW_ACTIVITY) {
                if (options_get_number(w->options, "monitor-activity"))
                        return (1);
        }
        if (flags & WINDOW_SILENCE) {
                if (options_get_number(w->options, "monitor-silence") != 0)
                        return (1);
        }
        return (0);
}

void
alerts_reset_all(void)
{
        struct window   *w;

        RB_FOREACH(w, windows, &windows)
                alerts_reset(w);
}

static void
alerts_reset(struct window *w)
{
        struct timeval  tv;

        if (!event_initialized(&w->alerts_timer))
                evtimer_set(&w->alerts_timer, alerts_timer, w);

        w->flags &= ~WINDOW_SILENCE;
        event_del(&w->alerts_timer);

        timerclear(&tv);
        tv.tv_sec = options_get_number(w->options, "monitor-silence");

        log_debug("@%u alerts timer reset %u", w->id, (u_int)tv.tv_sec);
        if (tv.tv_sec != 0)
                event_add(&w->alerts_timer, &tv);
}

void
alerts_queue(struct window *w, int flags)
{
        alerts_reset(w);

        if ((w->flags & flags) != flags) {
                w->flags |= flags;
                log_debug("@%u alerts flags added %#x", w->id, flags);
        }

        if (alerts_enabled(w, flags)) {
                if (!w->alerts_queued) {
                        w->alerts_queued = 1;
                        TAILQ_INSERT_TAIL(&alerts_list, w, alerts_entry);
                        window_add_ref(w, __func__);
                }

                if (!alerts_fired) {
                        log_debug("alerts check queued (by @%u)", w->id);
                        event_once(-1, EV_TIMEOUT, alerts_callback, NULL, NULL);
                        alerts_fired = 1;
                }
        }
}

static int
alerts_check_bell(struct window *w)
{
        struct winlink  *wl;
        struct session  *s;

        if (~w->flags & WINDOW_BELL)
                return (0);
        if (!options_get_number(w->options, "monitor-bell"))
                return (0);

        TAILQ_FOREACH(wl, &w->winlinks, wentry)
                wl->session->flags &= ~SESSION_ALERTED;

        TAILQ_FOREACH(wl, &w->winlinks, wentry) {
                /*
                 * Bells are allowed even if there is an existing bell (so do
                 * not check WINLINK_BELL).
                 */
                s = wl->session;
                if (s->curw != wl || s->attached == 0) {
                        wl->flags |= WINLINK_BELL;
                        server_status_session(s);
                }
                if (!alerts_action_applies(wl, "bell-action"))
                        continue;
                notify_winlink("alert-bell", wl);

                if (s->flags & SESSION_ALERTED)
                        continue;
                s->flags |= SESSION_ALERTED;

                alerts_set_message(wl, "Bell", "visual-bell");
        }

        return (WINDOW_BELL);
}

static int
alerts_check_activity(struct window *w)
{
        struct winlink  *wl;
        struct session  *s;

        if (~w->flags & WINDOW_ACTIVITY)
                return (0);
        if (!options_get_number(w->options, "monitor-activity"))
                return (0);

        TAILQ_FOREACH(wl, &w->winlinks, wentry)
                wl->session->flags &= ~SESSION_ALERTED;

        TAILQ_FOREACH(wl, &w->winlinks, wentry) {
                if (wl->flags & WINLINK_ACTIVITY)
                        continue;
                s = wl->session;
                if (s->curw != wl || s->attached == 0) {
                        wl->flags |= WINLINK_ACTIVITY;
                        server_status_session(s);
                }
                if (!alerts_action_applies(wl, "activity-action"))
                        continue;
                notify_winlink("alert-activity", wl);

                if (s->flags & SESSION_ALERTED)
                        continue;
                s->flags |= SESSION_ALERTED;

                alerts_set_message(wl, "Activity", "visual-activity");
        }

        return (WINDOW_ACTIVITY);
}

static int
alerts_check_silence(struct window *w)
{
        struct winlink  *wl;
        struct session  *s;

        if (~w->flags & WINDOW_SILENCE)
                return (0);
        if (options_get_number(w->options, "monitor-silence") == 0)
                return (0);

        TAILQ_FOREACH(wl, &w->winlinks, wentry)
                wl->session->flags &= ~SESSION_ALERTED;

        TAILQ_FOREACH(wl, &w->winlinks, wentry) {
                if (wl->flags & WINLINK_SILENCE)
                        continue;
                s = wl->session;
                if (s->curw != wl || s->attached == 0) {
                        wl->flags |= WINLINK_SILENCE;
                        server_status_session(s);
                }
                if (!alerts_action_applies(wl, "silence-action"))
                        continue;
                notify_winlink("alert-silence", wl);

                if (s->flags & SESSION_ALERTED)
                        continue;
                s->flags |= SESSION_ALERTED;

                alerts_set_message(wl, "Silence", "visual-silence");
        }

        return (WINDOW_SILENCE);
}

static void
alerts_set_message(struct winlink *wl, const char *type, const char *option)
{
        struct client   *c;
        int              visual;

        /*
         * We have found an alert (bell, activity or silence), so we need to
         * pass it on to the user. For each client attached to this session,
         * decide whether a bell, message or both is needed.
         *
         * If visual-{bell,activity,silence} is on, then a message is
         * substituted for a bell; if it is off, a bell is sent as normal; both
         * mean both a bell and message is sent.
         */

        visual = options_get_number(wl->session->options, option);
        TAILQ_FOREACH(c, &clients, entry) {
                if (c->session != wl->session || c->flags & CLIENT_CONTROL)
                        continue;

                if (visual == VISUAL_OFF || visual == VISUAL_BOTH)
                        tty_putcode(&c->tty, TTYC_BEL);
                if (visual == VISUAL_OFF)
                        continue;
                if (c->session->curw == wl) {
                        status_message_set(c, -1, 1, 0, 0,
                            "%s in current window", type);
                } else {
                        status_message_set(c, -1, 1, 0, 0,
                            "%s in window %d", type, wl->idx);
                }
        }
}