Надоедливые нотификации и как с ними бороться

Опубликовано 31.07.2025 в OpenBSD

Вот я не раз уже говорил: хочешь узнать, как сделать что-то максимально плохо и неудобно, посмотри, как сделано в MacOS. Хуже придумать точно уже не получится. А хочешь сделать хорошо и удобно - сначала детализируй требования.

Положим, вот нотификации. В почтовый клиент приходит почта, в мессенджеры пишут разнообразные люди (а также не люди и даже иногда не́люди), случаются и иные ситуации, когда приложению нужно привлечь к себе внимание пользователя, который в этот момент с оным приложением не работает и графического интерфейса его не видит. В MacOS у тебя при этом в панельке запуска значок приложения начнет подпрыгивать, как придурошный, само приложение нарисует тебе всплывающий баннер где-нибудь в углу экрана (а то и не один), да еще для пущей радости есть аналог "трея", где иные приложения тоже рисуют свои значки и всячески оными будут привлекать твое внимание. И звуковой сигнал еще чирикнет.

Отдельно остановлюсь на этих самых всплывающих нотификациях, пагубная мода на которые проникла просто во все сферы электронных коммуникаций. Вот объективно, вам действительно нужно срочно видеть, что Вася Пупкин написал вам в мессенджере "займи сотку до получки" и немедленно на это реагировать? Нет, ну всякие случаи бывают, я понимаю: кто-то, наверное, на бирже торгует и должен реагировать на изменения котировок быстрее бота или, там, людей спасает и призван, в этой связи, немедля взвиться с криком: "Андрюха, у нас труп, по ко́ням!"©. Но вот мне лично в 100% случаев достаточно лишь самого факта "в мессенджере кто-то написал" или "пришло электрическое письмо". Чтобы я, когда мне оно будет комфортно, закончив все текущие дела и, не тратя лишнего времени на постоянные переключения контекста, открыл на нужную программу и там посмотрел кто и что от меня хочет. Я вообще отлил бы этот принцип в граните: "пишешь в мессенджере или электронной почте - не жди немедленной реакции, у адресата могут быть более актуальные дела, более того, они наверняка у него и есть, так что адресат прочитает и отреагирует когда (и если) ему это будет удобно".

В общем, к детализации требований: мне необходимо и достаточно знать, что определенная программа (мессенджер, браузер, почтовый клиент и т.п.) желает, чтобы я обратил на нее внимание. Больше мне вот прямо немедленно ничего знать не надо. Программа должна лишь уметь просигналить "требую внимания", а менеджер окон должен единообразным и ненавязчивым способом довести до меня информацию "окно такой-то программы нуждается в твоем внимании". Что случилось, кто мне пишет, что пишет - вот вообще в моменте не имеет значения.

Соответственно, исходная: у меня в оконном менеджере i3 на отдельных рабочих пространствах запущены определенные программы. Запущены всегда. И всегда на одних и тех же: на первом пара терминалов, на втором браузер, на третьем клиент Telegram, на шестом почтовый клиент и так далее... Порядок, привычный мне за годы, всегда один и тот же. И оконный менеджер i3 умеет мне сигнализировать, что программа на таком-то рабочем пространстве требует моего внимания - для чего в панельке оконного менеджера (я использую стороннюю - polybar) значок соответствующего рабочего пространства подсвечивается заданным в конфиге цветом. Работает это так: программа сигнализирует, что ей необходимо внимание, используя фишку протокола X11, известную как "UrgencyHint flag", вот как она описывается в стандарте ICCCM:
https://tronche.com/gui/x/icccm/sec-4.html#s-4.1.2.4

Соответственно, для меня ситуация "в панельке подсвечен значок рабочего пространства номер 3" однозначно трактуется как "мне кто-то что-то написал в мессенджере Telegram". И этого мне, повторюсь, абсолютно достаточно, это всё, что мне требуется знать - ни больше, ни меньше. И мне именно так максимально удобно.

Теперь немножечко деталей реализации.

Проще всего с эмуляторами терминала. Запускаешь какую-то долго отрабатывающую консольную утилиту и хочешь получить нотификацию, когда исполнение завершится? Добавь ; echo "\а" к строке запуска своей команды, это и будет указание терминалу приковать к себе внимание по завершению выполнения. Хочешь получать информацию только об успешном или наоборот, неудачном завершении работы консольной утилиты? Используй, соответственно, && echo "\a" или || echo "\a". Для краткости можно alias создать, вроде такого: alias uh='echo -n "\a"' .

Следующий пациент - нативный клиент мессенджера Telegram. Тут, к счастью, все достижимо без особых приседаний: в настройках находим раздел "Notifications and Sounds", выключаем там тоггл "Desktop notifications", но оставляем включенным "Draw attention to the window". Прочее по своему усмотрению.

Теперь почтовый клиент. Я использую Claws-mail и там штатный модуль Notification имеет опции "Set window manager urgency hint when new message exist" и "Set window manager urgency hint when unread message exist". Имеет смысл включить обе опции, так как при получении почты по протоколу IMAP почта может помечаться как "непрочитанная", но не помечаться, как "новая". Вообще, любой приличный почтовый клиент имеет опцию типа "запускать команду при получении почты" и желаемого поведения можно добиться создав для запуска скрипт типа wmctrl -r имя_программы -b add,demands_attention (предварительно, конечно, потребуется установить инструмент wmctrl).

И, наконец, браузер - в моем случае, Mozilla Firefox используется время от времени для запуска веб-версий всяческих мессенджеров, под которые у меня нет нативного клиента (например, хваленый "национальный мессенджер МАХ"). Вот тут всё несколько сложнее: для Firefox есть, например, расширение, реализующее установку того самого UrgencyHint flag, вот оно:
https://addons.mozilla.org/en-US/firefox/addon/notification-attention/
https://github.com/CyberShadow/notification-attention

Но неприятная особенность заключается в том, что в Firefoх реализована своя собственная система рисования этих вот мерзостных всплывающих баннеров при получении веб-пуш нотификаций, и баннеры эти он рисует в виде окошек, воспринимаемых как "не управляемые менеджером окон" - i3 натуральным образом игнорирует появление такого окна (это видно, если подписаться на события посредством i3-msg -t subscribe -m '[ "window" ]') и, соответственно, никакие указания for_window в конфиге i3, ориентированные на WINDOW_ROLE или WINDOW_CLASS, работать просто не будут.

Я не нашел более изящного решения этой проблемы (хотя может оно и есть), чем убивать эти всплывающие баннеры через методы непосредственно X-сервера, для чего наговнокодил файлик close_notification.c следующего содержания:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
// подключим необходимые X11 заголовки
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <X11/Xutil.h>

// класс окна (а точнее instance, так как xprop показывает роль "Alert", "firefox" )
#define CLASS_MATCH "Alert"
// роль окна, согласно показаниям xprop
#define ROLE_MATCH "alert"

Display *display = NULL;

int main() {
    // Открываем соединение с X-сервером, с обработкой ошибок
    if ((display = XOpenDisplay(NULL)) == NULL) {
        fprintf(stderr, "Ошибка подключения к X-серверу!\n");
        return 1;
    }

    Window root = DefaultRootWindow(display);

    // Получаем атомы для свойств окон
    Atom net_wm_window_type = XInternAtom(display, "_NET_WM_WINDOW_TYPE", False);
    Atom notification_type = XInternAtom(display, "_NET_WM_WINDOW_TYPE_NOTIFICATION", False);
    Atom wm_window_role = XInternAtom(display, "WM_WINDOW_ROLE", False);
    Atom wm_class = XInternAtom(display, "WM_CLASS", False);

    // Слушаем события создания окон
    XSelectInput(display, root, SubstructureNotifyMask);

    XEvent event;
    while (1) {

        XNextEvent(display, &event);

        // Обрабатываем событие создания окна
        if (event.type == MapNotify) {
            XMapEvent *map_event = (XMapEvent *)&event;
            Window win = map_event->window;

            // Буферы для свойств
            Atom actual_type;
            int actual_format;
            unsigned long nitems, bytes_after;
            unsigned char *data = NULL;
            int match = 1;

            // Проверяем класс окна (xprop говорит "Alert", "firefox")
            if (XGetWindowProperty(display, win, wm_class, 0, 1024,
                                  False, XA_STRING, &actual_type, &actual_format,
                                  &nitems, &bytes_after, &data) == Success && data) {
                // но возвращается следующий формат данных: "instance\0class\0"
                char *instance = (char *)data;
                char *win_class = instance + strlen(instance) + 1;

                if (strstr(instance, CLASS_MATCH) == NULL) {
                    match = 0;
                }
                XFree(data);
            } else {
                match = 0;
            }

            if (!match) continue;

            // Проверяем роль окна ("alert")
            if (XGetWindowProperty(display, win, wm_window_role, 0, 1024,
                                  False, XA_STRING, &actual_type, &actual_format,
                                  &nitems, &bytes_after, &data) == Success && data) {
                char *role = (char *)data;
                if (strcmp(role, ROLE_MATCH) != 0) {
                    match = 0;
                }
                XFree(data);
            } else {
                match = 0;
            }

            if (!match) continue;

            // Проверяем тип окна (NOTIFICATION)
            if (XGetWindowProperty(display, win, net_wm_window_type, 0, 1024,
                                  False, XA_ATOM, &actual_type, &actual_format,
                                  &nitems, &bytes_after, &data) == Success && data) {
                Atom *atoms = (Atom *)data;
                int found = 0;
                for (unsigned long i = 0; i < nitems; i++) {
                    if (atoms[i] == notification_type) {
                        found = 1;
                        break;
                    }
                }
                XFree(data);
                if (!found) match = 0;
            } else {
                match = 0;
            }

            // Если все условия выполнены - закрываем окно
            if (match) {
                XDestroyWindow(display, win);
                XFlush(display);
            }
        }
    }

    // В обычных условиях сюда вроде бы не попадем?
    XCloseDisplay(display);
    return 0;
}

Далее компилируем полученное посредством

clang -I/usr/X11R6/include -L/usr/X11R6/lib -o close_notifications close_notifications.c -lX11

и прописываем в конфиг i3 запуск полученной программки при старте:

exec --no-startup-id $HOME/close_notifications

Вуаля, я получил единообразные и ненавязчивые нотификации об окнах, требующих привлечения моего внимания.