Началось все с переезда на новую версию одного дистрибутива Linux, а там — скандально известный GNOME Shell (GH для краткости), на Javascript. Ну ок, на JS так на JS, работает — и ладно. Одновременно с этим темп моей работы давно уже требовал найти нормальный почтовик, вместо тормозящей и жрущей тонны мегабайт вкладки outlook.office.com в браузере. И вот нашел, в наше время есть несколько почти прекрасных кандидатур, одна беда — почтовик стал доставать меня уведомлениями о новых письмах — и звуком, и всплывающими надписями. Что делать? Решение написать расширение "Не беспокоить" пришло не сразу, очень не хотелось писать велосипед и/или увязнуть в разработке/коде/тоннах ошибок, но решился, и вот хочу поделиться с Хабром своим опытом.1  Хочется иметь одну большую кнопку, чтобы выключить уведомления и звуки на время по выбору: 20 минут, 40 минут, 1 час, 2 часа, 4, 8 и 24 часа.2 Ага, тайминг как в Slack. На просторах extensions.gnome.org нашлось расширение "Do Not Disturb Button", которое послужило моделью к написанию своего расширения Do Not Disturb Time.  Установить с сайта extensions.gnome.org.
Исходники на github: ставим звездочки, форкаем, предлагаем улучшения. Создадим расширение с нуля:  Файл extension.js в соответствующей директории является входной точкой в нашем приложении, в минимальном исполнении он выглядит так: Для начала мы хотим добавить кнопку в Status Menu справа сверху, как на скриншоте выше. Итак, с чего бы начать? О, начнем с документации. У нас же есть официальная документация, все дела. А вот нет, официальная документация очень невелика и разрозненна, однако благодаря julio641742 и его неофициальной документации мы получаем то, что нужно: Данный код создает ключевой объект dndButton класса PanelMenu.Button — это кнопка, специально предназначенная для панели Status Menu. И мы ее вставляем в эту панель с помощью функции Main.panel.addToStatusArea().3 Вставляем пункты меню с прикрученными к ним обработчиками, пример: Спасибо тебе, julio641742, за документацию! Ссылка:
https://github.com/julio641742/gnome-shell-extension-reference Итоговый работающий код — по ссылке. На дворе конец 2018-го, и Node.js/V8 — основной инструмент для запуска Javascript-кода. Вся современная web-разработка держится на "ноде". Но GNOME Shell и вся инфраструктура вокруг него использует другой Javascript-движок, SpiderMonkey от Mozilla, и отсюда следует много важных различий в работе. В отличие от Node.js, здесь нет require(), и модного ES6-import-а — тоже. Вместо этого есть специальный объект imports, обращение к атрибутам которого приводит к загрузке модуля: В данном случае мы загрузили модуль js/ui/panelMenu.js из библиотеки пакета GNOME Shell, в котором реализован функционал кнопки с всплывающим меню.  Да-да, все кнопки в панели современного десктопа Linux, использующего GNOME, написаны на базе panelMenu.js. В том числе: та самая правая кнопка с индикаторами батареи, Wi-fi, громкостью звука; переключалка языка ввода en-ru. Далее, есть особый атрибут imports.searchPath — это список путей (строк), где будут искаться наши JS-модули. К примеру, мы выделили в отдельный модуль timeUtils.js функционал работы с таймером и положили его рядом с входной точкой нашего расширения, extension.js. Импортим timeUtils.js следующим образом: Ну раз у нас не Node.js, то и логирование у нас свое. Вместо console.log() в коде доступны несколько функций логирования, см. gjs/../global.cpp, static_funcs: и это нарисует в stderr в стиле: А вот отладчика для SpiderMonkey из коробки нет (не зря же я кропотливо выписал выше все доступные инструменты для логирования, пользуйтесь!). При желании можно попробовать JSRDbg: раз, два.  Есть. Полнофункциональные приложения, включая графический интерфейс (GUI), можно писать на Javascript! Запускать их нужно с помощью бинаря gjs, пускальщика JS-GTK-кода, пример создания GUI-окошка: Выше я упомянул про разбиение кода на модули и загрузку их из Javascript. Возникает вопрос, а как в самом модуле определить, запущен ли он как "main"-модуль, или загружен из другого модуля?  В Python-е есть аутентичная конструкция: В Node.js — аналогично: Официального ответа на этот вопрос для Gjs/GH я не нашел, но придумал такой прием, которым спешу поделиться с читателем (а че, кто-то дочитал "досюдова"? респект!). Итак, хитрый прием основан на анализе текущего стека вызовов, если он состоит из 2х и более строк — значит мы не в main()-модуле: Каждое расширение GNOME Shell имеет доступ ко всем объектам всего GNOME Shell. К примеру, чтобы отобразить кол-во непрочитанных еще уведомлений, доберемся до контейнера с ними в Notification Area, расположенного по центру сверху, номер 4 на картинке (нажмите на надпись с текущим временем, она кликабельна в реале, не здесь):  Можно узнать, сколько их, непрочитанных уведомлений, подписаться на события добавления и удаления уведомлений: Это прекрасно, но пользователь, бывает, может решить, что расширение X ему больше не нужно, и нажмет кнопку отключить расширение. Для расширения это равносильно вызову функции disable(), и нужно предпринять все усилия, чтобы выключенное расширение не поломало работающий GH: В данном случае, помимо того, что удаляем саму кнопку, нужно отписаться от событий "actor-added"/"actor-removed", пример: Если этого не сделать, то код обработчиков будет продолжать вызываться на соответствующего события, пытаться обновлять состояние несуществующей уже кнопки с менюшкой и… GNOME Shell начнет глючить. Ну да, напакостим мы, ругаться будут пользователи, камни полетят в разрабов GNOME Shell и GNOME в целом. Реальная картина, че. Итак, GNOME Shell/Gjs представляет собой симбиоз двух систем, Glib/GTK и Javascript, и у них разный подход к управлению ресурсами. Glib/GTK требует явного освобождения своих ресурсов (кнопок, таймеров и прочего). Если же объект создан движком Javascript-а, то действуем как обычно (ничего не освобождаем). В итоге, как только наше расширение готово, и не "течет", можно смело публиковать его на https://extensions.gnome.org.  Если вы еще не забыли, мы делаем расширение "Do Not Disturb", которое выключает показ уведомлений пользователю.  В GNOME уже есть флаг, отвечающий за это состояние. После логина пользователя создается процесс gnome-session, в котором этот флаг и находится: это атрибут GsmPresencePrivate.status, см. исходники gnome-session, gnome-session/gsm-presence.c. Доступ к этому флагу получаем через DBus-интерфейс (такое межпроцессное взаимодействие).  Не только нам, но и самому GH нужна информация об этом флаге, чтобы не показывать уведомления. Это достаточно легко ищется в исходниках GH: В данном случае метод _onStatusChanged есть обработчик, реагирующий на смену состояния. Копируем этот код к себе, адаптируем — с уведомлениями разобрались4, остался звук. Большинство современных Linux-десктопов управляется PulseAudio, небезызвестное поделие программа за авторством небезызвестного Lennart Poettering. До сих пор у меня не доходили руки пошерстить код PulseAudio, и я был рад представившейся возможности разобраться в PulseAudio на некотором уровне. В итоге оказалось, что для mute/unmute достаточно одной утилиты pactl, а точнее трех команд на ее основе: Итак, наша задача состоит в запуске команд/процессов, чтении их вывода stdout и поиске нужных значений по регулярке. Короче, стандартная задача. В GNOME за создание процессов отвечает базовая библиотека glib, и есть отличная документация по ней. И конечно она на C. А у нас JS. Известно, что пакет Gjs сделал умную, "интуитивно-понятную" прослойку между С-API и Javascript. Но все равно понимаешь, что нужны примеры и без гугления не обойтись. В итоге, благодаря прекрасному gist-у получаем работающий код: Не, конечно реестра в Linux-е нет. Тут вам не Windows. Есть лучше, называется GSettings (это API), за ним скрывается несколько вариантов реализации, по умолчанию в GNOME используется Dconf. Вот так выглядит GUI-шка для него:   — Чем это лучше хранения настроек в обычных текстовых файлах? — спросят олдовые и бородатые пользователи Linux-а. Основная фишка GSettings в том, что можно легко подписаться на изменения в настройке, пример: Единственная пока настройка в нашем "Do Not Disturb" — это опция "mute-audio", которая позволяет по желанию пользователя выключать или нет звук на время "тихого часа". Чтобы красиво показать пользователю настройки нашего расширения (а не лезть грязными лапами в реестр), GH предлагает нам написать GUI-код и положить его в функцию buildPrefsWidget() файла prefs.js. В этом случае напротив нашего расширения в списке "Installed Extensions" здесь мы увидим дополнительную кнопку "Configure this extension", по нажатию которой наша красота и появится. Давайте создадим отдельную вкладку About, ведь известно, что без "Эбаута", пардон, программа не является полноценной. Вообще говоря, для построения классического графического интерфейса у GTK есть весь ассортимент строительных блоков, виджетов, заценить кол-во и качество которых можно здесь. Мы же воспользуемся лишь несколькими из них: Итоговый скриншот:  Работа программиста предполагает 2 режима в моем случае:
1) в режиме поддержки, когда нужно быстро реагировать на события,- почта, Slack, Skype и прочее
2) в режиме работы, когда жизненно необходимо вырубить уведомления хотя бы на 20 минут, иначе фокус теряется и итоговая производительность труда — ничтожна. Именно для этого полезен режим "Не беспокоить". Может показаться, что полное выключение звука, mute, это слишком. Действительно, ведь в идеале хочется, чтобы в режиме "Не беспокоить" звонки Slack/Skype были слышны, а вот остальные звуки (реальных уведомлений) — нет. Но для этого их нужно как-то различать. Можно, конечно, сделать звуковое API специально для уведомлений (и такое уже есть), только вот всегда найдется программа/программист, который не задействует такой функционал. Пример — почтовик Mailspring: звуки он просто проигрывает через тег audio, и их никак не отличишь, допустим, от речи в звонке Slack-а. PanelMenu.Button — это собственно кнопка в панели + всплывающее меню, и можно самому разобраться и создать с нуля, и то, и другое, пацаны на раене оценят! У меня же была нацеленность на быстрый результат и поэтому я копипастнул код из неофициальной документации. Собственно инициировать смену режима нужно с помощью SetStatusRemote().