Hacking the Code#

This page contains some guidelines as well as a few tips on how to get started with the source code. If you feel like relevant information is missing, please open an issue on github.

If any of the following recommendations would stop you from contributing, please ignore them and contribute anyway. This is meant as help, not as show stopper!

In case you have questions, feel free to contact me directly or open an issue on github.

Getting the Source Code#

To retrieve the source code you can clone the repository using git:

$ git clone https://github.com/karlch/vimiv-qt.git

Submitting Changes#

The preferred way to submit changes is to fork the repository and to submit a merge request.

Running the Development Version#

The arguably cleanest method is to install vimiv using tox and use the script from the virtual environment:

$ .venv/bin/vimiv

If you have the dependencies installed globally, you have two other options:

  1. Build the c-extension in place and run the vimiv module directly from the repository directory:

    $ python setup.py build_ext --inplace
    $ python -m vimiv
    
  2. Install the development version globally using:

    $ python setup.py develop
    

    In contrast to the other options, this replaces your global vimiv installation and removes the option to have both a development and a production executable.

For running in a clean directory, use the --temp-basedir option. To change the log level, use the --log-level option, e.g. --log-level debug to enable debugging messages. As this can become very noisy, the --debug flag is useful to debug individual modules, e.g. --debug config.configfile.

Tests and Checkers#

Vimiv runs it’s tests using tox. There are various different environments:

  • The standard test environment using pytest. To run it with the latest PyQt version, use:

    tox -e pyqt
    

    This requires xorg xvfb to be installed on your system.

  • A linting environment to check the code quality and style using pylint, pycodestyle and pydocstyle. Run it with:

    tox -e lint
    
  • An environment to check the package for best-practices and completeness using pyroma and check-manifest. It can be run with:

    tox -e packaging
    
  • The mypy environment for static type checking launched with:

    tox -e mypy
    

In case you don’t want to run any of the checkers locally, you can just wait for the CI to run them. This is much slower and less direct though.

Style and Formatting#

Vimiv uses pre-commit for a consistent formatting. To format the python source code, the black code formatter is used.

You can install the tools with:

pip install pre-commit
pip install black

And setup pre-commit using:

pre-commit install

Using Qt#

As vimiv supports multiple Qt versions and wrappers simultaneously, none should be imported directly. Instead, use the corresponding import from vimiv.qt directly.

How to import Qt modules#

Good

Bad

from vimiv.qt.core import QTimer

from PyQt5.QtCore import QTimer

from vimiv.qt.core import Signal

from PyQt5.QtCore import pyqtSignal

from vimiv.qt.core import Slot

from PyQt5.QtCore import pyqtSlot

from vimiv.qt.widgets import QLabel

from PyQt5.QtWidgets import QLabel

from vimiv.qt.gui import QPixmap

from PyQt5.QtGui import QPixmap

Writing Plugins#

A great way to contribute to vimiv without having to work with the main source code is to write plugins. If you end up writing a plugin, please let me know so I can advertise it on the Plugins page. The basic usage of plugins on described in the Plugins page as well, some hints on the plugin infrastructure are given in the vimiv.plugins module:

Interface to load and initialize plugins.

Plugins are python modules that are either in vimiv/plugins/ (app plugins) or $XDG_DATA_HOME/vimiv/plugins/ (user plugins). A possible path is to write a plugin in its own git repository and let the user to clone that repository into $XDG_DATA_HOME/vimiv/plugins/. It can then be activated in the configuration file. A simple example to get an idea of the plugin structure is realized in the demo plugin vimiv/plugins/demo.py.

There are three main components a plugin can make use of to interact with vimiv:

  • The application api imported via from vimiv import api

  • The init function of the plugin which gets called as soon as the plugin is loaded. It receives the information string as first argument which contains the additional information supplied by the user in the configuration file after the plugin name. This can be used to receive simple information from the user.

  • The cleanup function of the plugin which gets called when the vimiv application exits.

Hint

It is considered good practice to add *args, **kwargs to the init and cleanup function of any plugin. This allows additional information to be passed via these functions at any time without breaking the plugin.

Note

Using any other imports from vimiv besides the api module is not considered stable and may break at any point without warning. If you require functionality that is not within the api, please open an issue to discuss this. This helps the api grow and keeps plugins stable.

Warning

Before the release of version 1.0 there may be changes to the api although it is tried to keep them as minimal as possible. Any breaking changes will be announced in advance to allow plugins to adapt.

The plugin loading process can be summarized by the following steps:

  1. The ‘PLUGINS’ section of the configuration file is iterated over. Keys defined are the names of the plugins that should be loaded later, values can be arbitrary additional information.

  2. After setting up the main application, the defined plugins are loaded by the load() function. During loading the init function of the plugin is called.

  3. Before the application is quit, the cleanup function of all loaded plugins is called.

vimiv.plugins.get_plugins() Dict[str, str]#

Retrieve dictionary containing active plugin names and additional information.

vimiv.plugins.load() None#

Load plugins defined.

If no plugins are passed to the function all active plugins are loaded.

Parameters:

plugins – Plugin names to load.

Adding Support for New Imageformats#

If you would like to add support for a new image format that is not supported by Qt, you can also solve this using the plugin system. A nice example of how to do this is the RawPrev plugin which adds support for viewing raw images using dcraw.

To make this work, you need to implement two functions:

  1. A function which checks if a path is of your filetype.

  2. The actual loading function which creates a QPixmap from the path.

Finally, you tell vimiv about the newly supported filetype:

from typing import Any, BinaryIO

from PyQt5.QtGui import QPixmap

from vimiv import api


def test_func(header: bytes, file_handle: BinaryIO) -> bool:
    """Return True if the file is of your format."""

def load_func(path: str) -> QPixmap:
    """Implement your custom loading here and return the created QPixmap."""

def init(_info: str, *_args: Any, **_kwargs: Any) -> None:
    """Setup your plugin by adding your file format to vimiv."""
    api.add_external_format("fmt", test_func, load_func)

Source Code Hints#

The following paragraphs explain some of the core concepts of the source code which may be useful to understand before working on bigger changes.

Logging#

Logging is handled by the vimiv.utils.log module which wraps around the standard python logging library:

Utilities related to logging.

There are two different types of loggers: the application-wide logger and module specific loggers.

The application-wide logger is used for general messages meant for the user. All log messages with level larger than debug are also sent to the statusbar. To send a message to this logger, the utility functions debug(), info(), warning(), error() and critical() can be used. They are just very thin wrapper functions around the python logging functions.

For debugging it is recommended to use a module specific logger. This allows fine-tuning the amount of debug statements that should be displayed using the --debug flag. To create a module logger:

from vimiv.utils import log

_logger = log.module_logger(__name__)

and use this logger as usual:

_logger.debug("Performing some setup")
...
_logger.debug("Doing the work")
...
_logger.debug("Performing some clean-up")

Three log handlers are currently used:

  • One to print to the console

  • One to save the output in a log file located in $XDG_DATA_HOME/vimiv/vimiv.log

  • One to print log messages to the statusbar (only for application-wide logger)

In case you want to ensure that a log message is only logged a single time, pass once=True to your log statement:

_logger.debug("Something happened, and may now happen very often", once=True)

API Documentation#

Utilities to interact with the application.

The following paragraphs provide an overview of the modules available in the api and give examples on how to use them.

The Object Registry#

Storage system for objects.

The object registry is a storage system for long-lived objects. These objects are stored and identified using their type. Therefore every stored object must be unique in its type and only one instance of each type can be stored. Purpose of this registry is to define an interface used by commands as well as statusbar modules to retrieve the self argument for methods that require an instance of the class.

To register a new class for this purpose, the register() decorator can be used as following:

from vimiv.api import objreg

class MyLongLivedClass:

    @objreg.register
    def __init__(self):
        ...

This class is now ready to provide commands and statusbar modules using the regular decorators. In principle, you can now retrieve the instance of the class via:

my_instance = MyLongLivedClass.instance

This is not recommended though and considered an implementation detail. The preferred method is to keep track of the instance otherwise.

vimiv.api.objreg.register(component_init: Callable) Callable#

Decorator to register a class for the object registry.

This decorates the __init__ function of the class to register. The object is stored in the registry right after __init__ was called.

Parameters:

component_init – The __init__ function of the component.

Modes#

Default modes and utility functions for mode handling.

Similar to vim, vimiv has the concept of modes. The same command or keybinding can perform different actions depending on the mode it is executed in. Each mode is assigned to a QWidget class which is focused when this mode is active. To assign a widget to a mode, the widget() decorator is used.

The following modes exist:

  • IMAGE

  • LIBRARY

  • THUMBNAIL

  • COMMAND

  • MANIPULATE

In addition there is the special GLOBAL mode which corresponds to IMAGE, LIBRARY and THUMBNAIL. When adding commands for this mode, they are automatically added for each of these three modes.

All modes inherit from the common Mode base class.

vimiv.api.modes.current() Mode#

Return the currently active mode.

vimiv.api.modes.get_by_name(name: str) Mode#

Retrieve Mode class by name.

This can be used in case the python vimiv.api.modes.Mode class is not available, for example when running commands.

Parameters:

name – Name of the mode to retrieve.

Returns:

The corresponding vimiv.api.modes.Mode class.

vimiv.api.modes.widget(mode: Mode) Callable#

Decorator to assign a widget to a mode.

The decorator decorates the __init__ function of a QWidget class storing the created component as the widget associated to the mode. This is used when entering a mode to focus the widget which is assigned to this mode.

Example:

class ImageWidget:

@modes.widget(modes.IMAGE)
def __init__(self):
    ...
Parameters:

mode – The mode to associate the decorated widget with.

Commands#

Command storage and initialization.

The user interacts with vimiv using commands. Creating a new command is done using the register() decorator. The command name is directly inferred from the function name, the functions docstring is used to document the command. The arguments supported by the command are also deduced by inspecting the arguments the function takes. To understand these concepts, lets add a simple command that prints “hello earth” to the terminal:

from vimiv.api import commands

@commands.register()
def hello_earth():
    print("hello earth")

This code snippet creates the command :hello-earth which does not accept any arguments. To allow greeting other planets, let’s add an argument name which defaults to earth:

@commands.register()
def hello_planet(name: str = "earth"):
    print("hello", name)

Now the command :hello-planet is created. When called without arguments, it prints “hello earth” as before, but it is also possible to great other planets by passing their name: :hello-planet --name=venus.

Hint

Type annotating the arguments is required as the type annotation is passed to the argument parser as type of the argument.

It is possible for commands to support the special count argument. count is passed by the user either by prepending it to the command like :2next or by typing numbers before calling a keybinding. Let’s update our :hello-planet command to support count by printing the greeting count times:

@commands.register()
def hello_planet(name: str = "earth", count: int = 1):
    for i in range(count):
        print("hello", name)

Another special argument is the paths argument. It will perform unix-style pattern matching using the glob module on each path given and return a list of matched paths. An example of this in action is the :open command defined in vimiv.api in the open_paths function.

Each command is valid for a specific mode, the default being global. To supply the mode, add it to the register decorator:

@commands.register(mode=Modes.IMAGE)
def ...

In general commands are usable by keybindings and in the command line. If it makes no sense for a command to be visible in the command line, e.g. the :command command which enters the command line, the hide option can be passed:

@commands.register(hide=True)
def ...

In this case it is probably smart to define a default keybinding for the command.

exception vimiv.api.commands.ArgumentError#

Raised if a command was called with wrong arguments.

exception vimiv.api.commands.CommandError#

Raised if a command failed to run correctly.

exception vimiv.api.commands.CommandInfo#

Raised if a command wants to show the user an info.

exception vimiv.api.commands.CommandNotFound#

Raised if a command does not exist for a specific mode.

exception vimiv.api.commands.CommandWarning#

Raised if a command wants to show the user a warning.

vimiv.api.commands.exists(name: str, mode: Mode) bool#

Check if a command exists in the registry.

Parameters:
  • name – Name of the command to check for.

  • mode – The mode for which the command is valid.

Returns:

True if the command exists.

vimiv.api.commands.get(name: str, mode: Mode = Mode.GLOBAL) _Command#

Get one command object.

Parameters:
  • name – Name of the command to look for.

  • mode – Mode in which to look for the command.

Returns:

The Command object asserted with name and mode.

vimiv.api.commands.items(mode: Mode) ItemsView[str, _Command]#

Retrieve all items in the commands registry for iteration.

Parameters:

mode – The mode for which the commands are valid.

Returns:

typing.ItemsView allowing iteration over items.

vimiv.api.commands.register(mode: Mode = Mode.GLOBAL, hide: bool = False, store: bool = True, edit: bool = False, name: str = None) Callable[[FuncT], FuncT]#

Decorator to store a command in the registry.

Parameters:
  • mode – Mode in which the command can be executed.

  • hide – Hide command from command line.

  • store – Save command to allow repeating with ‘.’.

  • edit – Command may make changes on disk.

  • name – Name of the command if it should not be inferred from the function name.

Keybindings#

Utilities to map commands to a sequence of keys.

Adding a new default keybinding is done using the register() decorator. This decorator requires the sequence of keys to bind to as first argument, the command as second argument and, similar to vimiv.api.commands.register() supports the mode keyword to define the mode in which the keybinding is valid.

As an example, let’s bind the :hello-earth command from before to the key sequence ge:

from vimiv.api import commands, keybindings

@keybindings.register("ge", "hello-earth")
@commands.register()
def hello_earth():
    print("hello earth")

If the keybinding requires passing any arguments to the command, these must be passed as part of the command. For example, to great venus with gv and earth with ge we could use:

@keybindings.register("gv", "hello-planet --name=venus")
@keybindings.register("ge", "hello-planet")
@commands.register()
def hello_planet(name: str = "earth"):
    print("hello", name)

Status Modules#

Utilities to add status modules and retrieve status text.

Status objects in vimiv, e.g. the statusbar displayed at the bottom, are configurable using so called status modules. These are created using the module() decorator. As an example let’s create a module that returns the name of the current user:

from vimiv.api import status

@status.module("{username}")
def username():
    return os.getenv("USER")

A new module ‘{username}’ is now registered.

Any status object can retrieve the content of statusbar modules by calling evaluate(). To get the content of our new “{username}” module prepended by the text “user: “ we run:

updated_text = status.evaluate("user: {username}")

The occurrence of ‘{username}’ is then replaced by the outcome of the username() function defined earlier.

If any other object requires the status to be updated, they should call vimiv.api.status.update() passing the reason for the requested update as string.

vimiv.api.status.clear(reason: str) None#

Emit signal to clear messages.

This function can be called when any temporary logging messages should be cleared.

Parameters:

reason – Reason of the clearing for logging.

vimiv.api.status.evaluate(text: str) str#

Evaluate the status modules and update text accordingly.

Replaces all occurrences of module names with the output of the corresponding function.

Example

A module called {pwd} is associated with the function os.pwd. Assuming the output of os.pwd() is “/home/user/folder”, the text ‘Path: {pwd}’ becomes ‘Path: /home/user/folder’.

Parameters:

text – The text to evaluate.

Returns:

The updated text.

vimiv.api.status.module(name: str) Callable[[ModuleFunc], ModuleFunc]#

Decorator to register a function as status module.

The decorated function must return a string that can be displayed as status. When calling evaluate(), any occurrence of name will be replaced by the return value of the decorated function.

Parameters:

name – Name of the module as set in the config file. Must start with ‘{’ and end with ‘}’ to allow differentiating modules from ordinary text.

vimiv.api.status.update(reason: str) None#

Emit signal to update the current status.

This function can be called when an update of the status is required. It is, for example, always called after a command was run.

Parameters:

reason – Reason of the update for logging.

Completion#

Utilities to work with completion modules.

A completion module offers a model with options for command line completion and a filter that decides which results are filtered depending on the text in the command line. All completion models inherit from the BaseModel class.

A completion module must define for which command line text it is valid. In addition, it can provide a custom filter as well as custom column widths for the results shown. By default there is only a single column which gets the complete width. Let’s start with a simple example:

from vimiv.api import completion

class UselessModel(completion.BaseModel):

    def __init__(self):
        super().__init__(":useless")  # Gets triggered when the command line text
                                      # starts with ":useless"
        data = [("useless %d" % (i),) for i in range(3)]
        self.set_data(data)

The model defined offers the completions “useless 0”, “useless 1” and “useless 2” if the command line text starts with “:useless”.

Note

The data which was set is a sequence of tuples. The tuples are required as it is possible to have multiple columns in the completion widget.

To include additional information on the provided completion, further columns can be added:

class UselessModel(completion.BaseModel):

    def __init__(self):
        super().__init__(":useless", column_widths=(0.7, 0.3))
        data = [("useless %d" % (i), "Integer") for i in range(3)]
        self.set_data(data)

The offered completions now provide and additional description (which is always “Integer”) that is shown in a second column next to the actual completion. The description column is set up to take 30 % of the total width while the completion takes 70 %.

For an overview of implemented models, feel free to take a look at the ones defined in vimiv.completion.completionmodels.

class vimiv.api.completion.BaseModel(text: str, column_widths: Tuple[float, ...] = (1,), valid_modes: Tuple[Mode, ...] = (Mode.GLOBAL, Mode.IMAGE, Mode.LIBRARY, Mode.THUMBNAIL, Mode.COMMAND, Mode.MANIPULATE))#

Base model used for completion models.

column_widths#

Tuple of floats [0..1] defining the width of each column.

modes#

Modes for which this completion model is valid.

on_enter(text: str) None#

Called by the completer when this model is entered.

This allows models to change their content accordingly.

Parameters:

text – The current text in the command line.

on_text_changed(text: str) None#

Called by the completer when the commandline text has changed.

This allows models to change their content accordingly.

Parameters:

text – The current text in the command line.

set_data(data: Iterable[Tuple]) None#

Add rows to the model.

Parameters:

data – List of tuples containing the data for each row.

Working Directory#

Handler to take care of the current working directory.

The handler stores the current working directory and provides the chdir() method to change it:

from vimiv.api import working_directory

working_directory.handler.chdir("./my/new/directory")

In addition the directory and current image is monitored using QFileSystemWatcher. Any changes are exposed via three signals:

  • loaded when the working directory has changed and the content was loaded

  • changed when the content of the current directory has changed

  • images_changed when the images in the current directory where changed

The first two signals are emitted with the list of images and list of directories in the working directory as arguments, images_changed includes the list of images as well as the list of added and removed images.

Thus, if your custom class needs to know the current images and/or directories, it can connect to these signals:

from vimiv.qt.core import QObject

from vimiv import api


class MyCustomClass(QObject):

    @api.objreg.register
    def __init__(self):
        super().__init__()
        api.working_directory.handler.loaded.connect(self._on_dir_loaded)
        api.working_directory.handler.changed.connect(self._on_dir_changed)
        api.working_directory.handler.images_changed.connect(self._on_im_changed)

    def _on_dir_loaded(self, images, directories):
        print("Loaded new images:", *images, sep="\n", end="\n\n")
        print("Loaded new directories:", *directories, sep="\n", end="\n\n")

    def _on_dir_changed(self, images, directories):
        print("Updated images:", *images, sep="\n", end="\n\n")
        print("Updated directories:", *directories, sep="\n", end="\n\n")

    def _on_im_changed(self, new_images, added, removed):
        print("Updated images:", *new_images, sep="\n", end="\n\n")
        print("Added images:", *added, sep="\n", end="\n\n")
        print("Removed images:", *removed, sep="\n", end="\n\n")
Module Attributes:

handler: The initialized WorkingDirectoryHandler object to interact with.

Prompt#

Prompt the user for a question.

vimiv.api.prompt.ask_question(*, title: str, body: str) Any#

Prompt the user to answer a question.

This emits the question_asked signal which leads to a gui prompt being displayed. The UI is blocked until the question was answered or aborted.

Parameters:
  • title – Title of the question.

  • body – Sentence body representing the actual question.

Returns:

Answer given by the user if any.

Return type:

answer

Imutils Module#

Utilities to load, edit, navigate and save images.

The Image Loading Process#

The image loading process is started by emitting the load_images signal. It is for example emitted by the library when a new image path was selected, in thumbnail mode upon selection or by the :open command. There are a few different cases that are taken care of:

  • Loading a single path that is already in the filelist In this case the filelist navigates to the appropriate index and the image is opened.

  • Loading a single path that is not in the filelist The filelist is populated with all images in the same directory as this path and the path is opened.

  • Loading multiple paths The filelist is populated with these paths and the first file in the list is opened.

To open an image the new_image_opened signal is emitted with the absolute path to this image. This signal is accepted by the file handler in vimiv.imutils._file_handler which then loads the actual image from disk using QImageReader. Once the format of the image has been determined, and a displayable Qt widget has been created, the file handler emits one of:

  • pixmap_loaded for standard images

  • movie_loaded for animated Gifs

  • svg_loaded for vector graphics

The image widget in vimiv.gui.image connects to these signals and displays the appropriate Qt widget.

Image Editing Modules#

This section gives a quick overview of the modules that deal with image editing and how to add new functionality to manipulate images.

imtransform - transformations such as rotate and flip.

immanipulate - more complex image manipulations like brightness and contrast.

The module includes classes in a hierarchical structure where each following class includes components of the previous class in the hierarchy.

  1. Manipulation stores a single manipulation such as brightness.

  2. ManipulationGroup stores manipulations that are applied together such as brightness and contrast.

  3. Manipulations stores all manipulation groups and provides commands to apply them.

  4. Manipulator stores the manipulations class and interfaces it with the application.

Adding new manipulations is done by implementing a new ManipulationGroup and adding it to the Manipulations.

class vimiv.imutils.immanipulate.ManipulationGroup(*manipulations: Manipulation)#

Base class for a group of manipulations associated to one manipulation tab.

The group stores the individual manipulations, associates them to a manipulate function and provides a title.

To implement a new manipulation group:

  • Inherit from this base class and define an appropriate constructor

  • Define the title() property

  • Implement the abstract method _apply()

manipulations#

Tuple of individual manipulations.

abstract _apply(data: bytes, *manipulations: Manipulation) bytes#

Apply all manipulations of this group.

Takes the image data as raw bytes, applies the changes according the current manipulation values and returns the updated bytes. In general this is associated with a call to a function implemented in the C-extension which manipulates the raw data.

Must be implemented by the child class.

Parameters:

data – The raw image data in bytes to manipulate.

Returns:

The updated raw image data in bytes.

apply(data: bytes) bytes#

Apply manipulation function to image data if any manipulation changed.

Wraps the abstract _apply() with a common setup and finalize part.

property changed: bool#

True if any manipulation has been changed.

abstract property title#

Title of the manipulation group as referred to in its tab.

Must be defined by the child class.

Adding New Manipulations to the C-Extension#

To add new manipulations to the C-extension, two things must be done.

First, you implement a new manipulate function in its own header such as brightness_contrast.h. The function should take the image data as pure bytes, the size of the data array as well as your new manipulation values as arguments. Task of the function is to update the data with the manipulation values accordingly. For this it needs to iterate over the data and update each pixel and channel (RGBA) accordingly.

Once you are happy with your manipulate function, it needs to be registered in manipulate.c. First you write a wrapper function that converts the python arguments to C arguments, runs the manipulate function you just implemented and returns the manipulated data back to python. How this is done can be seen in manipulate_bc and manipulate_hsl. The basic structure should be very similar for any case. Finally you add the python wrapper function to the ManipulateMethods definition. Here you define the name of the function as seen by python, pass your function, the calling convention and finally a short docstring.

For much more information on extending python with C see the python documentation