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:
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
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.
Good |
Bad |
---|---|
|
|
|
|
|
|
|
|
|
|
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:
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.
After setting up the main application, the defined plugins are loaded by the
load()
function. During loading theinit
function of the plugin is called.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:
A function which checks if a path is of your filetype.
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 ofname
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 loadedchanged
when the content of the current directory has changedimages_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 imagesmovie_loaded
for animated Gifssvg_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.
Manipulation
stores a single manipulation such as brightness.ManipulationGroup
stores manipulations that are applied together such as brightness and contrast.Manipulations
stores all manipulation groups and provides commands to apply them.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()
propertyImplement 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