Skip to content

Contributing Plugins

Lorenz Hoffmann edited this page Oct 23, 2022 · 6 revisions

If you want to join the growing list of contributors this is the place to start. Maybe you are missing some Plugins or catched a bug you would like to resolve on your own.

If you haven't done this already, set up your project:

  • clone the repository
  • create a virtualenv with python (optional)
  • make sure you have all requirements installed
  • since v3.1.0, you need systemd and the systemd-python package

There are two ways to change the theme in an application:

  1. Developing a script that changes config files or runs a command (a plugin in Yin & Yang)
  2. Develop an extension for the application where you want to change the theme, and trigger it by communicating with Yin & Yang

🧩 Plugins

Plugins provide a way to directly change the theme by either running a command or changing a config file.

Each plugin is a class in src/plugins and extends the class Plugin in _plugin.py. There are some subclasses of the Plugin class that you should use if suitable:

  • PluginCommandline: A plugin which switches the theme through a terminal command:
class MyPlugin(PluginCommandline):
    def __init__(self):
        super().__init__(['terminal-command', '--set', '{theme}'])
        self.theme_light = 'light theme'
        self.theme_dark = 'dark theme'

⚠️ Note that by default, the availability is termined by calling command[0] --help. If that call fails, the application assumes that the plugin is not available and hides it from the GUI. You can override this behaviour if needed.

  • PluginDesktopDependent: A plugin that behaves differently on different desktops:
class MyPlugin(PluginDesktopDependent):
    def __init__(self, desktop: Desktop):
        match desktop:
            case Desktop.KDE:
                super().__init__(_Kde())
            case Desktop.GNOME:
                super().__init__(_Gnome())
            case _:
                super().__init__(None)
class _Kde(Plugin):
    pass  # write a plugin for KDE here

class _Gnome(Plugin):
    pass  # write a plugin for Gnome here
  • ExternalPlugin: Used for communicating with other scripts, for example browser plugins. Does nothing except present itself in the UI and provides a config.

If none of the above subclasses seem suitable, subclass the Plugin class and write your code to set any theme by overwriting the method set_theme(). Make sure that all preconditions are correct, e.g.:

if not (self.available and self.enabled):
    return

if not theme:
    raise ValueError(f'Theme \"{theme}\" is invalid')

For changing config files, there are some helper methods in _plugin.py:

  • inplace_change(), which replaces a given line by a new one
  • get_stuff_in_dir(), which returns all files or directories in a path

For JSON files, I recommend the json module python provides, for config files similar to INI you should use the config parser module.

For dbus calls, you should use the qt dbus module (the KDE version of the wallpaper plugin can help you get started).

You also need to create an instance of your plugin in /src/plugins/__init__.py. Provide desktop if your plugin depends on the desktop. Now update the version in the default property of the config class. Finally, make sure your plugin works and all unit tests run successfully.

GUI

By default, each plugin has its own section in the plugins window, with one simple text input to set the desired theme.

If you want to provide a combo box instead, you can simply implement the property method available_themes. Any other widget can be used by overriding either

  • get_widget() if you want to edit the whole widget
  • get_input() if you only want to use different inputs for the light and dark theme. See wallpaper.py for example, it replaces the lineEdits with buttons.

Testing

Automatic tests check that your plugin has the expected behavior. However, to test that your set_theme() method works, you have to do the following:

  1. Enable your plugin (via UI or the config file stored in ~/.config/yin-yang/yin-yang.json).
  2. Change the False in tests/test_plugins.py: test_set_theme_works() to True. Make sure you do not commit this change later.
  3. Run tests/test_plugins.py.

📣 Communication

If you need to update the theme from an external application, you can do the following:

  1. Write a new yin-yang plugin and subclass ExternalPlugin.
  2. Call communicate.py as a process from your application.
  3. Write your application name into stdin of that process.
  4. Read the response from stdout. It should be a JSON object with the following data:
{
  "enabled": true,
  "dark_mode": true,
  "scheduled": true,
  "themes": ["light_theme", "dark_theme"],
  "times": [1615881600, 1615924800]
} 
  • enabled is true if your plugin is enabled. If not, the response only contains enabled and dark_mode
  • dark_mode is true if the system is currently using a dark theme
  • scheduled is true if the theme changes automatically. If not, the response does not contain the times section.
  • themes is a list of the preferred themes as strings.
  • times is a list of the times when the theme changes. These are Unix times in seconds since the epoch, and always "surround" the current time. This enables your external application to calculate the preferred theme directly and compare it to dark_mode if you want.

    For example, the times provided above would be the times when called on 2021-03-16 13:31:05.

Testing

To test if communicate.py gives the desired output, go to the directory where you cloned this repo and open a terminal. The following is an example output for Firefox:

$ python
> import communicate.py
> communicate.send_config('firefox')
{'enabled': True, 'dark_mode': True, 'scheduled': True, 'themes': ['firefox-compact-light@mozilla.org', 'firefox-compact-dark@mozilla.org'], 'times': [1620756000, 1620795600]}
Clone this wiki locally