Pull to refresh

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

Reading time 9 min
Views 4.3K
Я, как и многие, кого мог заинтересовать этот пост, люблю оболочку 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

На этом всё, спасибо за внимание.
Tags:
Hubs:
+6
Comments 3
Comments Comments 3

Articles