Как я вешал горячие клавиши на Unity sound indicator

от автора

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

Для тех кто не знает что такое этот sound indicator

Это значек с динамиком в панели индикаторов, сразу слева от часов:
image image

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

Те, кому не охота читать как всё это устроенно, могут проследовать ближе к концу статьи, или прямиком на github. Остальных прошу за мной.

С чего же можно начать такое предприятие?

Для начала, конечно же, стоит погуглить, что как ни странно никаких вразумительных результатов не даст — пара решений для конкретных плееров, да ссылка на баг о том, что при открытом индикаторе не работают горячие клавиши.
Следующим, очевидным шагом является поиск исходников этого компонета, дабы допилить напильником нехватающий функционал. Как оказалось, на официальном сайте unity есть всё необходимое, что бы достаточно оперативно эти самые исходники заполучить. Для этого надо перейти в раздел «Get involved», раз уж мы решили, что нам придеться быть вовлечеными во всё это. Далее стоит проследовать в раздел «Development», т.к. хотим мы собственно исходники, ну а раз мы хотим допилить стандартный компонент, то наш путь лежит в раздел «Common components». Где мы собственно и найдем наш долгожданный индикатор, хотя именно здесь, он почему то фигурирует под именем «Sound Menu», хотя в остальных частях системы он упоминается исключительно как indicator, ну да ладно.

Запилить, запилить немедленно

Качаем исходники…

bzr branch lp:indicator-sound 

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

Что мы имеем

Все исходники, могущие нас заинтересовать, как и ожидалось, лежат в директории src. Начнем как и водится с main.vala:

main.vala

[CCode (cheader_filename="libintl.h", type="char *")] extern unowned string bind_textdomain_codeset (string domainname, string codeset);  static int main (string[] args) { 	bind_textdomain_codeset (Config.GETTEXT_PACKAGE, "UTF-8"); 	Intl.setlocale (LocaleCategory.ALL, ""); 	Intl.bindtextdomain (Config.GETTEXT_PACKAGE, Config.GNOMELOCALEDIR);  	Notify.init ("indicator-sound");  	var service = new IndicatorSound.Service (); 	return service.run (); }  

Здесь мы увидим, что это не какое то классическое UI приложение, предоставляющее иконку в трее, а некая служба:

 var service = new IndicatorSound.Service ();

Раз оно служба, значит, всё необходимое для отрисовки и управления доступно по некоему интерфейсу, заглянем поглубже, а именно в service.vala. Строки вида:

//... 	void bus_acquired (DBusConnection connection, string name) {//...} //... 	void name_lost (DBusConnection connection, string name) {//...} //... 

несомненно, наводят на мысли что здесь замешана некая шина, а именно D-Bus. Бегло погуглив, можно понять что это вполне себе обьектно-ориентированный интрефейс, который, что приятно, можно дергать из консоли и, более того, мониторить то, как взаимодействуют различные службы. Так же имеются и GUI утилиты, например qdbusviewer, который можно установить коммандой:

sudo apt-get install qdbus-qt5

Далее стоит осмотреть внутренности метода run, который и вызываеться в main:

	public int run () { 		if (this.loop != null) { 			warning ("service is already running"); 			return 1; 		}  		Bus.own_name (BusType.SESSION, "com.canonical.indicator.sound", BusNameOwnerFlags.NONE, 			this.bus_acquired, null, this.name_lost);  		this.loop = new MainLoop (null, false); 		this.loop.run ();  		return 0; 	} 

В документации про метод own_name сказанно что то мало вразумительное, но выглядит это всё как регистрация на той самой шине.

Самое время поэксперементировать

Комманда gdbus имеет прекрасный и многообщающий метод introspect:

Посмотрим на наш сервис глазами системы

$ gdbus introspect --session --dest com.canonical.indicator.sound --object-path \ /com/canonical/indicator/sound  node /com/canonical/indicator/sound {   interface org.freedesktop.DBus.Properties {     methods:       Get(in  s interface_name,           in  s property_name,           out v value);       GetAll(in  s interface_name,              out a{sv} properties);       Set(in  s interface_name,           in  s property_name,           in  v value);     signals:       PropertiesChanged(s interface_name,                         a{sv} changed_properties,                         as invalidated_properties);     properties:   };   interface org.freedesktop.DBus.Introspectable {     methods:       Introspect(out s xml_data);     signals:     properties:   };   interface org.freedesktop.DBus.Peer {     methods:       Ping();       GetMachineId(out s machine_uuid);     signals:     properties:   };   interface org.gtk.Actions {     methods:       List(out as list);       Describe(in  s action_name,                out (bgav) description);       DescribeAll(out a{s(bgav)} descriptions);       Activate(in  s action_name,                in  av parameter,                in  a{sv} platform_data);       SetState(in  s action_name,                in  v value,                in  a{sv} platform_data);     signals:       Changed(as removals,               a{sb} enable_changes,               a{sv} state_changes,               a{s(bgav)} additions);     properties:   };   node desktop_greeter {   };   node phone {   };   node desktop {   }; }; 

Так как мы собираемся управлять этой службой, то, скорее всего, наиболее интересным для нас является интерфейс org.gtk.Actions. Эксперементировать предлагаю с плеером vkcom, хотя, по идее сгодится и любой другой. Давайте запустим примерно такую комманду:

dbus-monitor > monitor.log

И немного повзаимодействуем с нашим плеером, а именно нажмем Play, Pause, Next и Previous.

А теперь найдем результат наших действий в логе:

.... #Явно вызов воспроизведения, если судить по названию действия, значит где то ниже, будут отражены и остальные наши действия method call sender=:1.9 -> dest=:1.19 serial=27912 path=/com/canonical/indicator/sound; interface=org.gtk.Actions; member=Activate    string "play.vkcomvkcom.desktop"    array [    ]    array [    ] ... #И точно: ... #Next method call sender=:1.9 -> dest=:1.19 serial=27918 path=/com/canonical/indicator/sound; interface=org.gtk.Actions; member=Activate    string "next.vkcomvkcom.desktop"    array [    ]    array [    ] ... #Previous method call sender=:1.9 -> dest=:1.19 serial=27918 path=/com/canonical/indicator/sound; interface=org.gtk.Actions; member=Activate    string "previous.vkcomvkcom.desktop"    array [    ]    array [    ] ... 

А где же Pause спросите вы? Хм, а нетуть, есть только Play/Pause, зависящий от текущего состояния, которое мы конечно же придумаем как узнать. Предположения относительно org.gtk.Actions полностью себя оправдали, давайте теперь попробуем воспроизвести наши действия через другой интерфейс, нежели тот который мы имользовали при тыкании в индикатор, а именно через консольный:

#Play/Pause gdbus call --session --dest com.canonical.indicator.sound --object-path /com/canonical/indicator/sound \ --method org.gtk.Actions.Activate   'play.vkcomvkcom.desktop' [] {} #Next gdbus call --session --dest com.canonical.indicator.sound --object-path /com/canonical/indicator/sound \  --method org.gtk.Actions.Activate   'next.vkcomvkcom.desktop' [] {} #Previous  и т.д и т.п. ... 

Полный список действий можно узнать выполнив комманду:

gdbus call --session --dest com.canonical.indicator.sound --object-path /com/canonical/indicator/sound --method org.gtk.Actions.List 

Думаю по какому принципу они сформированны вы уже уловили.

Реализация

Круто, работает! Да, действительно в таком виде это уже можно использовать — для какого то конкретного плеера… Но, это не наш метод.
Не знаю как у вас, а у меня, обычно, установленно больше одного плеера, и хотелось бы управлять ими всеми. Для этого придеться придумать какой то механизм для переключения «текущего» плеера, ведь играть могут одновреммено несколько проигрывателей. Для этого, не мешало бы, иметь способ получить полный список проигрывателей, которые управляються indicator sound service. Немного погуглив, легко понять что этот список лежит в некой «dconf databse», манипулировать с которой можно с помощью утилиты dconf. Попробуем?

dconf read /com/canonical/indicator/sound/interested-media-players

Не правда ли не очень читаемо? А так:

dconf read /com/canonical/indicator/sound/interested-media-players | sed  -e "s:[],'\[]::g" -e "s:\s:\n:g"

Sed наше все. Ну и что, а как же теперь выбирать «текущий»? Ну, я решил эту проблему так:

Код

#Место где мы будем хранить информацию о текущем плеере UCS_CACHE=~/.cache/unity-control-sound UCS_CURRENT_PLAYER_FILE=$UCS_CACHE/current-player #Список зарегестрированных проигрывателей UCS_INTERESTED_PLAYERS=`dconf read \     /com/canonical/indicator/sound/interested-media-players \     | sed  -e"s:[],'\[]::g" ` #Список предпочитаемых проигрывателей UCS_PREFFERED_PLAYERS=`dconf read /com/canonical/indicator/sound/preferred-media-players \     | sed  -e "s:[],'\[]::g"`  mkdir -p $UCS_CACHE touch $UCS_CURRENT_PLAYER_FILE UCS_CURRENT_PLAYER=`cat $UCS_CURRENT_PLAYER_FILE`  function initialize-current-player {     #Если ранее записанный проигрыватель не встречаеться в списке зарегестрированных проигрывателей     if ! echo $UCS_INTERESTED_PLAYERS | grep -q $UCS_CURRENT_PLAYER ;     then         #Делаем текущим проигрывателем первый из предпочитаемых         UCS_CURRENT_PLAYER=`echo $UCS_PREFFERED_PLAYERS | grep -o "^\S*[^.]"`         UCS_CURRENT_PLAYER=`echo $UCS_CURRENT_PLAYER | sed "s/\s//g"`         echo Current player now is '"'$UCS_CURRENT_PLAYER'"'     fi }  #Мотаем проигрыватель на следующий function player-next {     initial_player=$UCS_CURRENT_PLAYER     for player in $UCS_INTERESTED_PLAYERS      do         if [ -z "$first_player" ];         then             first_player=$player         fi          if [ "$previous_player" == "$UCS_CURRENT_PLAYER" ];         then             UCS_CURRENT_PLAYER=$player             break         fi         previous_player=$player     done     if [ "$initial_player" == "$UCS_CURRENT_PLAYER" ];     then         UCS_CURRENT_PLAYER=$first_player     fi     echo $UCS_CURRENT_PLAYER > $UCS_CURRENT_PLAYER_FILE }  #По аналогии на предыдущий function player-previous {     initial_player=$UCS_CURRENT_PLAYER     for player in $UCS_INTERESTED_PLAYERS      do         if [ -z "$first_player" ];         then             first_player=$player         fi          if [ "$player" == "$UCS_CURRENT_PLAYER" ];         then             UCS_CURRENT_PLAYER=$previous_player         fi         previous_player=$player     done     if [ -z "$UCS_CURRENT_PLAYER" ];     then         UCS_CURRENT_PLAYER=$previous_player     fi     echo $UCS_CURRENT_PLAYER > $UCS_CURRENT_PLAYER_FILE } 

Какой никакой, а интерфейс для переключения проигрывателей мы получили. Но хотелось бы что бы это ещё и выглядело прилично, и желательно как то проявляло себя на UI:

Код

#То, что мы видим как имя плеера в списке действий, это на самом деле, имя  #ярлыка, распологающегося по одному из этих путей: UCS_SYSTEM_WIDE_LAUNCHERS_PATH=/usr/share/applications UCS_LAUNCHERS_PATH=~/.local/share/applications  #Найти полный путь для ярлыка заданного плеера function player-launcher {     name=$1     launcher=$UCS_LAUNCHERS_PATH/$name      system_wide_launcher=$UCS_SYSTEM_WIDE_LAUNCHERS_PATH/$name      if [ -f "$launcher" ];     then         echo $launcher      else         echo $system_wide_launcher      fi }  #Прочитать красивое имя проигрывателя из его ярлыка function player-display-name {     name=$1     launcher=`player-launcher $name`     if [ -f "$launcher" ];     then         cat $launcher | grep -m 1 "^Name=" \             | sed "s/Name=//"     else         echo $player | sed "s/.desktop//"     fi  }  #Выдрать оттуда же иконку, если таковая имееться function current-player-icon {     launcher=`player-launcher $UCS_CURRENT_PLAYER`     if [ -f "$launcher" ];     then         cat $launcher | grep "Icon=" \             | sed "s/Icon=//"     fi }  #Вывести всю имеющуюся информацию в терминал и в виде уведомления function show-current-player {     echo Curent player '"'$UCS_CURRENT_PLAYER'"'     for player in $UCS_INTERESTED_PLAYERS     do        if [ $player == $UCS_CURRENT_PLAYER ];        then            players=$players*        fi         player_name=`player-display-name $player`        players=$players$player_name\\n     done     icon=`current-player-icon`     if ! [ -z $icon ];     then         icon="-i $icon"     fi      echo Icon is "$icon"     notify-send "Players:" "$players" $icon -t 1 } 

Как вы возможно заметили, я использовал команду notify-send, для вывода данных о списке плееров в виде уведомления. Это я к тому, что по умолчанию в Ubuntu для этой комманды не работает флаг -t, обозначающий таймаут который будет показываться уведомление, поэтому уведомления показываються неприлично долго. Исправить это можно, если воспользоваться этими инструкциями. Кто то возможно скажет — это не unix way, смешивать получение данных и их вывод на UI, я соглашусь, но дабы не усложнять использование скрипта я сделал всё именно так.
Используя полученные выше навыки работы с gdbus, можно без труда реализовать весь остальной функционал по управлению проигрывателем, по этому на этом подробно останавливаться я не буду, с тем что получилось в итоге можно ознакомиться на github. Отдельно, хочу лишь упомянуть о реализации сбора информации о текущем треке. Информация о состоянии проигрывателя, как оказалось, хранится в состоянии действия (gtk.Action), которое отвечает за запуск проигрывателя. Получить эту информацию можно с помощью метода org.gtk.Actions.Describe, и делается это так:

gdbus call --session --dest com.canonical.indicator.sound --object-path /com/canonical/indicator/sound \ --method org.gtk.Actions.Describe vkcomvkcom.desktop 

В выводе этой команды содержиться минимальная необходимая информация о текущем состоянии проигрывателя и текущего трека, если таковой имееться.
После проделанной работы осталось только добавить горячие клавиши вызывающие действия из получившегося скрипта. Я использовал для этого CompizConfig, т.к. штатными средствами мне это не удалось проделать (Ubuntu 13.10), уж не знаю почему это не работает, но, надеюсь, вскоре это починят.
Установить CompizConfig можно испльзуя такую команду:

sudo apt-get install compizconfig-settings-manager

Выглядит это все примерно так:

Скриншоты

image
image
image

На этом всё, спасибо за внимание.

ссылка на оригинал статьи http://habrahabr.ru/post/203504/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *