skyr.at

Controlling spotify from the command line

In case you ever wondered how to control Spotify on Linux from the command line or you wanted to automate some processes, here's how: Spotify uses DBus to communicate with other processes and implements the Media Player Remote Interfacing Specification (short: MPRIS). This defines a few standard interfaces to control media players.

The following command toggles between play and pause:

dbus-send --type=method_call --dest=org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.PlayPause

Noteworthy are the dbus destination (org.mpris.MediaPlayer2.spotify), the object path (/org/mpris/MediaPlayer2) and the actual command: org.mpris.MediaPlayer2.Player.PlayPause.

To make things easier, I've written a small bash script for some common operations:

#!/bin/sh

DBUS_DEST=org.mpris.MediaPlayer2
DBUS_OBJ_PATH=/org/mpris/MediaPlayer2
PLAYER=spotify

if [[ $# -ne 1 ]]; then
  echo "Usage: $0 playpause|next|previous"
  exit 1
fi

if [[ "$1" == "playpause" ]]; then
  dbus-send --type=method_call --dest=$DBUS_DEST.$PLAYER $DBUS_OBJ_PATH $DBUS_DEST.Player.PlayPause
elif [[ "$1" == "next" ]]; then
  dbus-send --type=method_call --dest=$DBUS_DEST.$PLAYER $DBUS_OBJ_PATH $DBUS_DEST.Player.Next
elif [[ "$1" == "previous" ]]; then
  dbus-send --type=method_call --dest=$DBUS_DEST.$PLAYER $DBUS_OBJ_PATH $DBUS_DEST.Player.Previous
fi

You can also find this here.

There are quite a few things you can do over dbus, check out the MRPIS documentation for more information, but this should get you started.

SFML and CEGUI 0.8.3

If you ever wanted to use CEGUI with SFML you might have stumbled across this article by Laurent Gomila. Its last revision happened about a year ago, so it's kinda outdated. I've managed to use CEGUI 0.8.3 with SFML 2.1 and here's how:

Compiling & Linking

Sadly, there's no official cmake module for CEGUI, so either you write your own, use one of an existing project (like this one) or you link the libraries manually. The libraries you'll need to link is libCEGUIBase-0.so and libCEGUIOpenGLRenderer-0.so. If you need extra features, link the rest yourself (e.g. Lua support, dialogs, ...). I don't know if this concerns all Linux distributions, but at least my Arch puts CEGUI's includes into /usr/include/cegui-0, so I had to tell my compiler to use this as include directory (-I/usr/include/cegui-0).

Using CEGUI in SFML

My code layout is a bit different from the tutorial mentioned above, but you should be able to figure it out. We start with our GUI manager class:

#ifndef GAMEDESKTOP_H
#define GAMEDESKTOP_H

#include <SFML/System/Time.hpp>
#include <SFML/Graphics/RenderWindow.hpp>
#include <SFML/Window/Keyboard.hpp>
#include <SFML/Window/Mouse.hpp>

#include <CEGUI/CEGUI.h>
#include <CEGUI/RendererModules/OpenGL/GLRenderer.h>

class GameDesktop
{
  public:
    GameDesktop(sf::RenderWindow &screen);

    void handleEvent(const sf::Event &event);
    void update(sf::Time delta);
    void draw();

  private:
    void initializeKeyMap();
    CEGUI::Key::Scan toCEGUIKey(sf::Keyboard::Key code);
    CEGUI::MouseButton toCEGUIMouseButton(sf::Mouse::Button button);

    sf::RenderWindow& screen_;
    CEGUI::OpenGLRenderer& renderer_;

    std::map<sf::Keyboard::Key, CEGUI::Key::Scan> key_map_;
    std::map<sf::Mouse::Button, CEGUI::MouseButton> mouse_map_;
};

#endif // GAMEDESKTOP_H

All we want to keep as members is our OpenGLRenderer and the WindowManager. SFML keycodes have changed their namespace to Keyboard, so we use sf::Keyboard::Code instead of sf::Key::Code.

My implementation looks like this:

#include <SFML/Window/Event.hpp>
#include <SFML/Graphics.hpp>

#include "gamedesktop.h"

GameDesktop::GameDesktop(sf::RenderWindow &screen)
  : screen_(screen),
    renderer_(CEGUI::OpenGLRenderer::bootstrapSystem())
{
  // Set up default resource groups
  CEGUI::DefaultResourceProvider *rp = static_cast<CEGUI::DefaultResourceProvider*>(CEGUI::System::getSingleton().getResourceProvider());

  rp->setResourceGroupDirectory("schemes", "/usr/share/cegui-0/schemes/"); 
  rp->setResourceGroupDirectory("imagesets", "/usr/share/cegui-0/imagesets/");
  rp->setResourceGroupDirectory("fonts", "/usr/share/cegui-0/fonts/");
  rp->setResourceGroupDirectory("layouts", "/usr/share/cegui-0/layouts/");
  rp->setResourceGroupDirectory("looknfeels", "/usr/share/cegui-0/looknfeel");
  rp->setResourceGroupDirectory("lua_scripts", "/usr/share/cegui-0/lua_scripts/");

  CEGUI::ImageManager::setImagesetDefaultResourceGroup("imagesets");
  CEGUI::Font::setDefaultResourceGroup("fonts");
  CEGUI::Scheme::setDefaultResourceGroup("schemes");
  CEGUI::WidgetLookManager::setDefaultResourceGroup("looknfeels");
  CEGUI::WindowManager::setDefaultResourceGroup("layouts");
  CEGUI::ScriptModule::setDefaultResourceGroup("lua_scripts");

  // Set up the GUI
  CEGUI::SchemeManager::getSingleton().createFromFile("WindowsLook.scheme");
  CEGUI::FontManager::getSingleton().createFromFile("DejaVuSans-10.font");

  CEGUI::System::getSingleton().getDefaultGUIContext().getMouseCursor().setDefaultImage("WindowsLook/MouseArrow");

  CEGUI::WindowManager& wmgr = CEGUI::WindowManager::getSingleton();
  CEGUI::Window *root = wmgr.createWindow("DefaultWindow", "root");
  root->setProperty("MousePassThroughEnabled", "True");
  CEGUI::System::getSingleton().getDefaultGUIContext().setRootWindow(root);

  CEGUI::FrameWindow *fw = static_cast<CEGUI::FrameWindow*>(wmgr.createWindow("WindowsLook/FrameWindow", "testWindow"));

  root->addChild(fw);
  fw->setText("Hello World!");

  // Initialize SFML-to-CEGUI key mapping
  initializeKeyMap();
}

void GameDesktop::handleEvent(const sf::Event &event)
{
  bool handled = false;
  CEGUI::GUIContext& context = CEGUI::System::getSingleton().getDefaultGUIContext();
  sf::Vector2i mouse_pos;

  switch (event.type)
  {
  case sf::Event::TextEntered:
    handled = context.injectChar(event.text.unicode); break;
  case sf::Event::KeyPressed:
    handled = context.injectKeyDown(toCEGUIKey(event.key.code)); break;
  case sf::Event::KeyReleased:
    handled = context.injectKeyUp(toCEGUIKey(event.key.code)); break;
  case sf::Event::MouseMoved:
    mouse_pos = sf::Mouse::getPosition(screen_);
    handled = context.injectMousePosition(static_cast<float>(mouse_pos.x), static_cast<float>(mouse_pos.y));
    break;
  case sf::Event::MouseButtonPressed:
    handled = context.injectMouseButtonDown(toCEGUIMouseButton(event.mouseButton.button)); break;
  case sf::Event::MouseButtonReleased:
    handled = context.injectMouseButtonUp(toCEGUIMouseButton(event.mouseButton.button)); break;
  case sf::Event::MouseWheelMoved:
    handled = context.injectMouseWheelChange(static_cast<float>(event.mouseWheel.delta)); break;
  case sf::Event::Resized:
    CEGUI::System::getSingleton().notifyDisplaySizeChanged(CEGUI::Sizef(event.size.width, event.size.height)); break;
  }

  // If mouse is over an UI element, we do not initiate drag scrolling
  if (!handled)
  {
    // we can handle this event here, CEGUI didn't use it ...    
  }
}

void GameDesktop::update(sf::Time delta)
{
  CEGUI::System::getSingleton().injectTimePulse(delta.asSeconds());
}

void GameDesktop::draw()
{
  // undo all transformations SFML did
  screen_.resetGLStates();
  CEGUI::System::getSingleton().renderAllGUIContexts();
}

void GameDesktop::initializeKeyMap()
{
  key_map_[sf::Keyboard::A]         = CEGUI::Key::A;
  key_map_[sf::Keyboard::B]         = CEGUI::Key::B;
  key_map_[sf::Keyboard::C]         = CEGUI::Key::C;
  // (...)

  mouse_map_[sf::Mouse::Left]       = CEGUI::LeftButton;
  // (...)
}

CEGUI::Key::Scan GameDesktop::toCEGUIKey(sf::Keyboard::Key code)
{
  return key_map_[code];
}

CEGUI::MouseButton GameDesktop::toCEGUIMouseButton(sf::Mouse::Button button)
{
  return mouse_map_[button];
}

The constructor sets everything up, including paths used by CEGUI's theme and texture system. You might want to change these paths in your project. It's important to correctly set up your root window if you want to handle more than GUI events in the class GameDesktop. That's why we why call:

// (...)
root->setProperty("MousePassThroughEnabled", "True");
// (...)

I cut the initializeKeyMap() function, either download the source or look here. The important parts are in handleEvents() and update(). The first one handles the event injection CEGUI needs, to function properly, as the library itself has no way of handling input. This includes resizing of the RenderWindow. Instead of injecting our events directly into CEGUI::System, we now use a GUIContext.

CEGUI also needs to be informed about time to update animations etc. We solve this by injecting time pulses in update():

// (...)
CEGUI::System::getSingleton().injectTimePulse(delta.asSeconds());
// (...)

When drawing, we need to reset all transformations SFML has performed, to keep our windows from getting transformed too:

// undo all transformations SFML did
screen_.resetGLStates();
CEGUI::System::getSingleton().renderAllGUIContexts();

And that's it. Pretty straight forward, isn't it? :)

You can get the complete source code here. Compile it using:

$ g++ -g -o test -std=c++11 -I/usr/include/cegui-0 *.cpp -lsfml-window -lsfml-system -lsfml-window -lsfml-graphics -lCEGUIBase-0 -lCEGUIOpenGLRenderer-0

If you have questions, find bugs or want to say something else feel free to leave a comment.

Page 1 / 1