Спецификация системного трея для Linux

PHP  /  Linux Arch  /  Новые разработки  




Трей - специальный способ отображения программ на рабочем столе, когда у программы нет активного видимого окна, и она не отображается в панели задач, но может иметь ограниченный функционал отображения и управления в специальной области рабочего стола.

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



Концепция новой спецификации состоит в том, что существуют два вида приложений - которое хочет в трей (далее client), и которое хочет отображать трей (далее host).

Эти приложения должны работать независимо друг от друга, и с как можно меньшим количеством зависимостей в коде. Наиболее простой, надежный, и поддерживаемый большинством языков программирования способ обмена такими примитивными данными - файлы. При чем это могут быть не обязательно файлы блочной файловой системы, это может быть и виртуальная файловая система, к примеру tmpfs.



Опишем краткий способ взаимодействия двух приложений, client и host.



Client.

Приложение, которое хочет отобразиться в трее, должно создать в определенном месте файловой системы, несколько определенных файлов.

Наиболее удобным с точки зрения безопасности и универсальности местом, является /run/user, которое содержит директорию, названную именем UID, в сессию которого произошел вход.

Пункт 1. Создать директорию /run/user/UID/systray/PID, где UID - наш идентификатор пользователя, а PID - идентификатор нашего системного процесса;

Пункт 2. В директории создать несколько файлов с разными предназначениями

a) title - текстовый файл с названием программы

b) tooltip - текстовый файл с текстом всплывающей подсказки при наведении указателя

c) icon_name - текстовый файл с именем иконки из системной темы, которую необходимо отобразить

d) icon_pixbuf - файл текущей иконки в упрощенном формате RGBA с указанием размера иконки

24,24:R,G,B,A,R,G,B,A,R,G,B,A, ... и так 24*24 раз.

e) action - файл обратной связи, который будет использоваться для передачи команды приложению client

f) .updated - обыкновенный пустой файл, время модификации которого будет использовано для обновления данных как приложением client, так и приложением host.

Пункт 3. После исполнения этих действий, приложение должно держать палец на дате изменения файла /run/user/UID/systray/PID/.updated, и если оно изменится - то перечитать файл action - в нем должна быть команда для приложения.

Пункт 4. Опционально приложение может видоизменить иконку или текст, путем обыкновенной перезаписи файлов title, tooltip, icon_name или icon_pixbuf. После окончания записи, должно быть так же изменена дата изменения файла /run/user/UID/systray/PID/.updated - это даст сигнал host-приложению перечитать данные.

Пункт 5. При изменении даты изменения файла /run/user/UID/systray/PID/.updated - приложение может прочитать файл /run/user/UID/systray/PID/action, и реагировать в зависимости от содержимого данного файла. На данный момент поддерживается два вида событий

Activate - равнозначно основному нажатию на иконку приложения. Чаще всего оно активирует основное окно программы, хотя все зависит от логики работы

ContextMenu - равнозначно дополнительному нажатию на иконку приложения, чаще всего это правая кнопка мыши. Активирует контекстное меню программы с основными опциями.

Минимальный пример работы приложения-клиента на PHP с использованием библиотеки php-gtk3:


#!/system/php/bin/php
<?php
global $old_mtime, $mtime, $window;

function add_me_to_tray($my_name, $my_icon, $tooltip, $pixbuf=true) {
	global $mtime;
// Если директории в формате /run/user/CURRENT_USER/systray/PID не существует, то создаем ее
	if (!is_dir($_SERVER['XDG_RUNTIME_DIR']."/systray/".getmypid())) {
		mkdir($_SERVER['XDG_RUNTIME_DIR']."/systray/".getmypid(),0700,true);
	}
	if ($pixbuf==true) {
		file_put_contents($_SERVER['XDG_RUNTIME_DIR']."/systray/".getmypid()."/icon_pixbuf", $my_icon);
	} else {
		file_put_contents($_SERVER['XDG_RUNTIME_DIR']."/systray/".getmypid()."/icon_name", $my_icon);
	}
	touch($_SERVER['XDG_RUNTIME_DIR']."/systray/".getmypid()."/.updated");
	file_put_contents($_SERVER['XDG_RUNTIME_DIR']."/systray/".getmypid()."/title", $my_name);
	file_put_contents($_SERVER['XDG_RUNTIME_DIR']."/systray/".getmypid()."/tooltip", $tooltip);
	$mtime = filectime($_SERVER['XDG_RUNTIME_DIR']."/systray/".getmypid()."/.updated");
}

function get_action() {
	if (!is_file($_SERVER['XDG_RUNTIME_DIR']."/systray/".getmypid()."/action")) {return false;}
	$action=file_get_contents($_SERVER['XDG_RUNTIME_DIR']."/systray/".getmypid()."/action");
	return trim($action);
}
function check_for_actions($window)
{
	global $old_mtime, $mtime, $context_menu;
	$old_mtime = $mtime;	
	$mtime = filectime($_SERVER['XDG_RUNTIME_DIR']."/systray/".getmypid()."/.updated");
	if ($mtime!=$old_mtime) { // Если файл изменился - значит произошло событие
		if (get_action()=="Activate") { // И это событие - активация
			// Все последующее - для активации окна на передний план
			$window->set_visible(true);
			$window->show();
			$window->activate_focus();
			$window->present_with_time($time);
			$window->show();
		}
		if (get_action()=="ContextMenu") { // Создаем простейшее контекстное меню. Реализовано в форме окна, поэтому не закрывается по потере фокуса
			$display = new GdkDisplay();
			global $x,$y;
			$x = $display->get_mouse_positionX(); // Возле курсора мыши, само собой
			$y = $display->get_mouse_positionY();
			unset($display);
			$context_menu = new GtkWindow();
			$context_menu->set_type_hint(2);
			$context_menu->set_size_request(200, 200);
			$context_menu->set_decorated(false);
			$vbox = new GtkBox(GtkOrientation::VERTICAL);
			$vbox->set_border_width(0);
			$o_button = GtkButton::new_with_label("Open"); // Менюшка Open будет открывать наше окно
			$o_button->connect("clicked", function() {global $x,$y, $window, $context_menu;$time=time();$window->show();$window->present_with_time($time);$window->activate_focus();$context_menu->destroy();});
			$vbox->add($o_button);
			$o_button->show();
			$button = GtkButton::new_with_label("Close"); // Менюшка Close будет его закрывать.
			$button->connect("clicked", function() {Gtk::main_quit();});
			$vbox->add($button);
			$button->show();
			$context_menu->add($vbox);
			$context_menu->show();
			$vbox->show();
			$context_menu->move($x+22,$y+22); // Не на иконке же рисовать меню - нарисуем чуть правее и ниже
			$context_menu->activate_focus();
		}
	}
	clearstatcache(); // Пых кеширует date modify по умолчанию, надо чистить
}
Gtk::init();
$window = new GtkWindow();
$window->set_size_request(250, 250);
$window->set_title("SystemTrayExaple");
$window->set_decorated(true);
$vbox = new GtkBox(GtkOrientation::VERTICAL);
$vbox->set_border_width(1);
$label = new GtkLabel("Ну типа контент");
$vbox->add($label);
$button = GtkButton::new_with_label("Кнобка !");
$vbox->add($button);
$window->add($vbox);
$window->connect("destroy", function() {
	Gtk::main_quit();
});
$window->show_all();
// Добавляем нашу программку в трей
add_me_to_tray("Test App", "viber", "Test App в трее", false);
// Добавляем таймер для периодического чека
Gtk::timeout_add(150, function () {global $window; check_for_actions($window);return true;});
$window->set_visible(true);
Gtk::main();


Host.

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

Пункт 1. Приложение с определенной им самим периодичностью (оптимальным временем реакции является 100-250мс), читает все подкаталоги в директории /run/user/UID/systray/ - существующие каталоги, согласно данной спецификации, носят имя с PID client-приложения, которое захотело отобразиться в трее.

Пункт 2. Каждый найденный PID должен проверяться на наличие в системе, чтобы исключить обработку несуществующих программ. Если язык программирования не имеет в себе функций работы с процессами, то самый простой способ определить жив ли процесс - проверить существование директории с именем PID в директории /proc

Пункт 3. Если процесс найден, то проверяем, не изменилось ли время модицикации файла /run/user/UID/systray/.updated с момента предыдущей проверки. Если оно изменилось, значит приложение client обновило информацию, или же это новое приложение, что в идеале не имеет значения.

Пункт 4. Перечитываем информацию, поданную процессом PID, а именно файлы title, tooltip, icon_name и icon_pixbuf, если они существуют. На основании этой информации, отрисовываем элемент трея в произвольной форме. Чаще всего это иконка со всплывающей подсказкой, к которой привязана обработка нажатий указателя.

Пункт 5. Если PID не найден в системе, это значит что приложения добавившего себя в трей больше не существует, и его нужно удалить из трея и из области файловой системы. Упрощенно говоря командой rm -rf /run/user/UID/systray/PID

Пункт 6. При нажатии на иконку основным или дополнительным нажатием, host должен записать в соответствующий файл /run/user/UID/systray/PID/action действие Activate или ContextMenu, после чего обновить время изменения файла /run/user/UID/systray/.updated, чтобы client увидел что состояние изменилось, прочитал action и отреагировал на запрос.



Минимальный пример работы приложения-хоста на PHP с использованием библиотеки php-gtk3:


<?php
global $tray_box, $old_time, $mtime;
function rmrf($dir) {
   $files = array_diff(scandir($dir), array('.','..'));
    foreach ($files as $file) {
      (is_dir("$dir/$file")) ? delTree("$dir/$file") : unlink("$dir/$file");
    }
    return rmdir($dir);
  }
function check_tray()
{
global $tray_box, $old_time, $mtime, $item_button;
// Сперва получаем список всех директорий, и определяем какая из них отсутствует в /proc, чтобы прибить трей
$scan = scandir($_SERVER['XDG_RUNTIME_DIR']."/systray/");
	foreach ($scan as $process) {
		if (is_numeric($process)) {
			if (is_dir("/proc/$process")) { // Процесс все еще существует, можем смотреть, было ли изменено состояние
			$old_mtime[$process] = $mtime[$process];
			$mtime[$process] = filectime($_SERVER['XDG_RUNTIME_DIR']."/systray/".$process."/.updated");	
			if ($mtime[$process]!=$old_mtime[$process]) { // И только если наше гостевое приложение обновило свои данные в системном трее - тогда обновляем и мы
				if (is_file($_SERVER['XDG_RUNTIME_DIR']."/systray/".$process."/icon_name")) {$icon = file_get_contents($_SERVER['XDG_RUNTIME_DIR']."/systray/".$process."/icon_name");}
				if (is_file($_SERVER['XDG_RUNTIME_DIR']."/systray/".$process."/title")) {$title = file_get_contents($_SERVER['XDG_RUNTIME_DIR']."/systray/".$process."/title");}
				if (is_file($_SERVER['XDG_RUNTIME_DIR']."/systray/".$process."/tooltip")) {$tooltip = file_get_contents($_SERVER['XDG_RUNTIME_DIR']."/systray/".$process."/tooltip");}
				if (!isset($item_button[$process])) { // Если процесс новенький, то создаем под него кнопку
					$item_button[$process] = new GtkButton();
					$item_button[$process]->connect("button-press-event",function($button, $event) {process_click($button, $event);});
				} else { // Если процесс уже есть в трее, то удаляем внутренности кнопки, оставляя все остальное. GObject не умеет в удаление событий, а это уменьшит утечку памяти
					foreach ($item_button[$process]->get_children() as &$value) {
						$value->destroy();
					}	
				}
				$item_box[$process] = new GtkBox(GtkOrientation::HORIZONTAL);
				$image = GtkImage::new_from_icon_name("$icon", 5);
				$image->set_pixel_size(22);
				$item_box[$process]->add($image);
				$image->show();
				$item_button[$process]->add($item_box[$process]);
				$item_button[$process]->set_name($process);
				$item_button[$process]->set_relief(GtkReliefStyle::NONE);
				$item_button[$process]->set_has_tooltip(true);
				$item_button[$process]->set_tooltip_text($tooltip);
				$tray_box->add($item_button[$process]);
				$item_box[$process]->show();
				$item_button[$process]->show();
				$tray_box->show();
			}
			} else { // Процесс более не найден - прибиваем его
			rmrf($_SERVER['XDG_RUNTIME_DIR']."/systray/".$process);
			$item_button[$process]->destroy();
			}	
		}
	}	
}
function process_click($item, $event) {
	$process = $item->get_name();
	if ($event->button->button == 1) {file_put_contents($_SERVER['XDG_RUNTIME_DIR']."/systray/".$process."/action", "Activate");} // Нажали левую кнопку - записали Activate
	if ($event->button->button == 3) {file_put_contents($_SERVER['XDG_RUNTIME_DIR']."/systray/".$process."/action", "ContextMenu");} // Нажали правую - записали ContextMenu
	touch($_SERVER['XDG_RUNTIME_DIR']."/systray/".$process."/.updated");
	return false;
}
Gtk::init();
function GtkWindowDestroy($widget=NULL, $event=NULL)
{
	Gtk::main_quit();
}
if (!is_dir($_SERVER['XDG_RUNTIME_DIR']."/systray")) {mkdir($_SERVER['XDG_RUNTIME_DIR']."/systray",0700);} // Если мы запустились первыми в системе, и директории нет - создаем
$win = new GtkWindow();
$win->set_default_size(300, 200);
$win->connect("destroy", "GtkWindowDestroy");
$tray_box = new GtkBox(GtkOrientation::HORIZONTAL);
$win->add($tray_box);
$win->show_all();
Gtk::timeout_add(150, function () {check_tray();return true;}); // Держим палец на трее каждые 150 мс, используя ГыТыКа таймаут. Без паники, процессорное время тратится разве что на ls и stat.
Gtk::main();

Послесловие.


В чем преимущество этой спецификации по сравнению с уже существующими реализациями xembed и SNI (dbus)?


1. Простота использования и наглядная понятность принципа работы;
2. Возможность использования любого ЯП, как для клиента так и для хоста, потому что нет необходимости использовать сторонние технологии
и библиотеки, а библиотеки для работы с файлами есть практически в каждом языке;
3. Нет привязки к графическому серверу. Как хост, так и клиент могут работать с Xorg, Wayland, Framebuffer и с любой существующей и
потенциальной реализацией графики;
4. Возможность безболезненно дополнять функционал;
5. Возможность существования нескольких хостов в рамках одной сессии.



Метки текста: linux system tray, xembed, SNI


https://minidevices.top/images/ava.png
https://minidevices.top/images/ava.png
2024-02-07 21:07:12
root
 07 февраля 2024 в 21:07 
  0  

87
2024-02-07 21:07:12


Комментировать



Опубликовать запись
В этой строке мы предупреждаем Вас, что можем использовать так называемые cookies
Нам искренне плевать на введенную Вами информацию о себе. Мы просто запоминаем у Вас на устройстве то, что Вы же и настроили.