Функционал, который не давал мне покоя целый год

Опубликовано: 08.07.2025

— A где же люди? — вновь заговорил наконец Маленький принц. — В пустыне всё-таки одиноко…
— Среди людей тоже одиноко, — заметила змея.

На протяжении последних 6 месяцев я работал над большим проектом — своей системой виджетов. Как это часто бывает, очередной проект становится самым крупным, самым сложным, самым амбициозным. В один момент я добавил на сайт информацию о нём, потому что мне хотелось поделиться каким-то прогрессом, пусть и небольшим.

Этот пост также можно считать попыткой поделиться прогрессом, хотя в целом речь пойдёт о вполне конкретном функционале. Такой своеобразный devlog. А ещё он связан с предыдущим, в котором я рассказал о том, как в Linux реализован функционал рабочего стола.

Почему в названии речь про «целый год»?

Внимательный читатель может задать этот вопрос: ведь я сказал, что разрабатываю текущий проект полгода.

На самом деле, идея появилась у меня примерно год назад, когда я увидел одну из реализаций такой системы. Мне стало интересно узнать, как она работает, и я стал изучать. Спустя некоторое время, наверное, месяц, я попробовал написать прототип. И именно тогда я и столкнулся с «функционалом, который не давал мне покоя целый год».

Текущая кодовая база — это практически полностью Go. В отличие от неё, прототип был написан на Python. Кроме того, между его созданием и началом работы над самим проектом прошло около полугода, в основном это связано с учёбой. Поэтому началом работы над проектом я считаю конец января 2025, а не август 2024.

Функционал

Я и так достаточно сильно затянул с тем, чтобы объяснить, о чём вообще этот пост. И для этого я покажу вам картинку:

Системный трей

Да, речь пойдёт о системном трее

Системный трей — это область панели, которая отображает некоторые приложения. Пожалуй, первым в голову приходит Telegram — его иконка отображается в трее, сообщая пользователю, что мессенджер запущен.

Как бы мне ни хотелось, чтобы рабочие столы Linux был популярнее, приходится в качестве картинки использовать Windows, потому что так гораздо проще объяснить, о чём речь. Я изначально писал свою систему виджетов только для Linux и, возможно, для других UNIX-подобных систем — я сомневаюсь, что Windows позволит реализовать подобный функционал в принципе, а поддержка macOS потребовала бы создания отдельной реализации с нуля.

Ранее, в Hakutest’е, я уже реализовывал запуск приложения в системном трее — и это в целом не было сложной задачей. Но сам по себе трей реализовать сложнее. Мне хотелось, чтобы он присутствовал в панели, которая является частью моей системы виджетов.

Как работает трей?

В предыдущем посте я рассказал о D-Bus — механизме для межпроцессного взаимодействия, на котором основана значительная часть функционала рабочего стола в Linux. И трей не является исключением.

Стоит отметить, что существует два разных типа системных треев. Первый, более старый, основан на расширении xembed для оконной системы X11, позволяющем встраивать элементы управления одним приложением в другое (апплеты). Трей являлся специальным клиентом X-сервера, в который другие приложения встраивают свои апплеты. Второй — протокол org.kde.StatusNotifierItem, который использует D-Bus и не зависит от оконной системы. Даже если вам ничего не понятно — речь пойдёт только о втором, современном типе.

Трей состоит из трёх сущностей:

  1. Элемент (item) — это приложение, которое отображается в трее в виде иконки. Как правило, их несколько, более того, один процесс может иметь несколько иконок.
  2. Хост (host) — приложение, которое отображает элементы.
  3. Наблюдатель (watcher) — получает информацию от элементов, следит за ними и отправляет сигналы об изменении хосту.

В один момент времени в системе может быть запущено не более одного наблюдателя и любое количество элементов и хостов. Приложения вызывают метод наблюдателя org.kde.StatusNotifierWatcher.RegisterStatusNotifierItem. После этого наблюдатель отправляет сигнал org.kde.StatusNotifierWatcher.StatusNotifierItemRegistered, который сообщает о том, что появился новый элемент. Хост отслеживает этот сигнал и отображает приложение в трее. Наблюдатель следит за элементами: когда его имя освобождается из D-Bus, то есть отправляется сигнал org.freedesktop.DBus.NameOwnerChanged с пустым владельцем имени, он отправляет сигнал org.kde.StatusNotifierWatcher.StatusNotifierItemUnregistered. Хост отслеживает и его, убирая иконку из трея. При этом приложение может обновлять свой элемент: менять иконку, всплывающую подсказку и т.д., отправляя ряд сигналов. Это также отслеживается хостом и он меняет отображение.

Схема работы системного трея

Схема работы системного трея

Выглядит всё это непросто, даже со схемой. Зачастую, проще понять что-либо, если есть пример. Рассмотрим самый распространённый сценарий, где изначально в системе запущен наблюдатель и один хост:

  1. Приложение вызывает метод RegisterStatusNotifierItem наблюдателя.
  2. Наблюдатель отправляет сигнал StatusNotifierItemRegistered.
  3. Хост получает этот сигнал и отображает иконку приложения в трее.
  4. Приложение работает некоторое время и меняет свою иконку, отправляя сигнал NewIcon.
  5. Хост получает сигнал и обновляет отображение элемента.
  6. Приложение завершается, освобождая имя элемента в D-Bus.
  7. D-Bus отправляет сигнал NameOwnerChanged с пустым значением нового владельца имени.
  8. Наблюдатель получает этот сигнал.
  9. Наблюдатель отправляет сигнал StatusNotifierItemUnregistered.
  10. Хост получает этот сигнал и прекращает отображение этого приложения.

Всё это — основной принцип работы системного трея. Но есть кое-что, о чём я ещё не рассказал.

Меню

У подавляющего большинства приложений, которые работают в трее, есть контекстное меню. Обычно, оно открывается, если нажать на иконку правой кнопкой мыши.

Протокол StatusNotifierItem напрямую не реализует это меню. Для этого есть отдельный протокол — com.canonical.dbusmenu. По сути, он предоставляет метод для получения структуры меню. Она представляет собой дерево, где каждый элемент — это пункт меню, который может содержать дочерние пункты. При этом у элементов есть набор свойств: какой текст отображается в пункте меню, можно ли на него нажать и т.д.

Меню

Пример меню

При этом приложение может отправлять сигналы об обновлении меню: изменяться могут как меню целиком, так и структура или свойства отдельных элементов.

Моя реализация

Репозиторий GitHub .

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

Ещё одна особенность состоит в том, что элементы самостоятельно обновляют свои свойства, необходимые для отображения. Вместо ручного отслеживания можно зарегистрировать callback, который будет вызван при обновлении элемента. Это упрощает обновление отображения приложения при изменении иконки, всплывающей подсказки и т.д.

Помимо основного протокола org.kde.StatusNotifierItem, пакет реализует com.canonical.dbusmenu и позволяет получить информацию о контекстном меню приложения. Для графического отображения меню я также написал привязки для библиотеки dbusmenu-gtk3 ( ссылка ) — это отдельный пакет, который позволяет работать с контекстным меню как с виджетом GTK.

Используя созданные библиотеки, я реализовал модуль системного трея для своей панели:

Системный трей в моей панели

Но почему целый год?

Безусловно, системный трей — это не самый простой функционал. Особенно учитывая, что на Go до этого не было реализаций, и в целом язык редко используется для разработки графических интерфейсов. Но и год — это достаточно большой промежуток времени.

Как и всегда, дьявол кроется в деталях.

Стоит начать с того, что примерно половину этого времени я практически не работал над этим проектом — я описывал это ранее. Год назад лишь появилась идея написать системный трей. Тогда я попробовал реализовать его, но не достиг какого-либо результата.

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

Поэтому правильнее будет сказать, что я время от времени пытался реализовать трей в течение года. Этот функционал, определённо, не отпускал меня, отсюда и названия поста.

Далее я хотел бы сказать пару слов про спецификацию протокола. Это документ, описывающий все свойства, методы и сигналы сущностей. Я многое могу сказать про спецификации Freedesktop в целом. Некоторые из них, например, для уведомлений , составлены неплохо и даже понятно. Но бóльшую часть их спецификаций очень сложно понять, и эта является классическим примером. Порой складывается впечатление, что они написаны «для галочки». И я не единственный, кто так считает: даже сами разработчики из Freedesktop обсуждали возможность переписать спецификацию StatusNotifierItem, но до этого пока не дошло. Это ни в коем случае не умаляет их заслуг, но факт остаётся фактом — разобраться в этой спецификации было очень непросто, и это заняло много времени.

Впрочем, жаловаться на спецификацию основного протокола не очень корректно, ведь она, по крайней мере, есть. С контекстным меню всё обстоит гораздо хуже. Единственный ресурс, который я нашёл — это XML-файл с информацией об интерфейсе. Благо, в нём есть описания методов и сигналов, но в целом такие файлы используются для интроспекции и генерации кода, и в меньшей степени предназначены для людей.

Наконец, как это часто бывает, жизнь требует от нас стать лучше, чтобы достичь поставленной цели. И простого способа «стать лучше», к сожалению или счастью, не существует. Это всегда непросто, а порой и откровенно неприятно и даже больно. Но в конечном итоге это приводит к тому, что человек растёт как личность и берёт новые высоты, ранее не виданные.

Заключение

Разбираться в устройстве системного трея было интересно. Несмотря на неудачные попытки реализовать его, плохо составленные спецификации и малое количество примеров, у меня получилось. Наверное, реализация трея звучит гораздо менее интересно, чем какой-нибудь веб-сервис или приложение. И тем не менее, это опыт решения нестандартной задачи — увлекательный опыт.

Спасибо, что дочитали до конца!