How to write Nautilus extensions with nautilus-python

Nautilus, also known as “Files”, is the default file manager of the GNOME desktop environment. In a previous tutorial we saw how to create and call custom scripts from the Nautilus context-menu: this feature can be really useful but is somehow limited. By installing the nautilus-python package in our favorite Linux distribution, and writing just few lines of Python code, we can overcome such limitations and create proper Nautilus extensions.

In this tutorial we learn how to write Nautilus extensions using Python and the bindings provided by the nautilus-python package.

In this tutorial you will learn:

  • How to install nautilus-python on some of the most used Linux distributions
  • How Nautilus API works
  • How to create Nautilus extensions in Python
How to write nautilus extensions with nautilus-python
How to write Nautilus/Files extensions with nautilus-python
Software requirements and conventions used
Category Requirements, Conventions or Software Version Used
System Distribution independent
Software Nautilus/Files, nautilus-python, Python3
Other Administrative privileges to install packages, basic knowledge of Python and Object Oriented Programming
Conventions # – requires given linux-commands to be executed with root privileges either directly as a root user or by use of sudo command $ – requires given linux-commands to be executed as a regular non-privileged user

Installation

To be able to create Nautilus extensions using Python, the first thing we need to do is to install the nautilus-python package. The software is itself an extension which provides Python bindings for Nautilus. To install the package on Fedora we can launch the following command:

$ sudo dnf install nautilus-python

To perform the installation on Debian and other Debian-based Linux distribution, instead:

$ sudo apt install nautilus-python

The package is available also in the Archlinux “Community” repository. We can install it using pacman:

$ sudo pacman -Sy python-nautilus

Extensions API overview

Once the Python bindings for Nautilus are installed, we can start writing our first extensions. In the course of this article I will use the latest stable release of Fedora (36 at the moment of writing), which comes with version 42.2 of the file manager. Keep in mind that API may change, so the code you see in this article may require some adjustments in the future.



We can extend Nautilus in many ways: we can create custom actions which can be executed from the file manager context menu when certain type of files are selected, for example, but we can also display additional information about a file in dedicated property pages or in custom columns displayed in the Nautilus “list” view. Depending on what we want to accomplish, the class which is going to represent our extension, should inherit from one or more of the following base classes:

The APIs provide also a series of classes which represent some elements we can define:

Let’s see some practical examples of how we can use them to create our own Nautilus extensions.

Creating a context-menu extension

In this first example we create a custom-action extension. This extension adds a custom action which allows us to convert jpeg and png images to the webp format, directly from the Nautilus context-menu. Here is how the extension is defined:

"""
Add an action to the Nautilus context-menu to convert png and jpeg images
to the wepb format.

Author: Egidio Docile
"""

from urllib.parse import urlparse, unquote

from gi.repository import GObject, Nautilus
from PIL import Image


class ConvertToWebpMenuProvider(GObject.GObject, Nautilus.MenuProvider):
    VALID_MIMETYPES = ('image/png', 'image/jpeg')

    def convert(self, menu, files):
        for file in files:
            file_path = unquote(urlparse(file.get_uri()).path)
            image = Image.open(file_path)
            image.save(f"{file_path}.webp", format="webp")

    def get_file_items(self, window, files):
        for file in files:
            if file.get_mime_type() not in self.VALID_MIMETYPES:
                return ()

        menu_item = Nautilus.MenuItem(
                        name="convert_to_webp",
                        label="Convert to wepb")

        menu_item.connect('activate', self.convert, files)

        return menu_item,

The first thing we did was to import needed modules and functions. From the urllib.parse module we imported the urlunquote and urlparse functions (we will see why in a moment), than, from gi.repository we imported the GObject and Nautilus modules.

Our extension is represented by the ConvertToWebpMenuProvider class which is derived from GObject.GObject (all extensions must extend this class) and Nautilus.MenuProvider. The get_file_items method we implemented in the class is inherited from the latter and accepts two arguments which are automatically passed to it.



The first one, window, is an object which represents the parent Gtk window, the second, files, is a list of FileInfo objects, each one representing a selected file. The method must return a list of Nautilus.MenuItem objects which represent the additional menu items we want to add to the Nautilus context menu (our implementation actually returns a tuple).

In the method body we checked if any of the selected files has a MIME type which is not among the accepted ones: image/png and image/jpeg. The MIME type of a file is returned by the get_mime_type method of the FileInfo object which represents it. If some of the selected files has a MIME type which is not allowed, we return an empty tuple: in such case, no extra entries will appear in the Nautilus context menu. If all files have a valid MIME type, instead, we create an instance of the Nautilus.MenuItem class. The constructor of this class accepts four strings as arguments:

  1. name: the menu item identifier
  2. label: the label the user sees in the Nautilus context-menu
  3. tip: the tooltip for the menu entry
  4. icon: the name of the icon to be displayed in the menu entry

In our example we used only the first two arguments and omitted the last two, which are optional.

Once the menu item object is created, we call the connect method on it in order to associate a callback to the activate signal, which is emitted when the user clicks on the menu entry produced by our extension. The first argument passed to the connect method is the name of the signal, the second is the name of the callback function; the subsequent ones are the arguments which will be passed to the callback itself. In the example we used the convert method of our class as callback, and passed the list of the selected files as argument to it.

The convert method is where the image conversion is actually performed. How we converted the image is not important. What should be noticed is that we obtained the URI of the FileInfo object by using the get_uri method. Since we need just the “path” part of the URI, we passed it as argument to the urlparse function. This function parses the URI into 6 parts which can be accessed via the corresponding properties of the returned ParseResult object:

We further processed the path by passing it to the unquote function, which replaces %xx escapes by their single character equivalents. This is especially useful in case the path contains spaces.

The file containing the Python code for our extension must be copied in the ~/.local/share/nautilus-python/extensions directory (it must be created if doesn’t already exist). Here is how our custom menu action is displayed when we select one or more jpeg or png images and we open the Nautilus context menu:

Our "convert to webp" extension entry in the Nautilus context menu
Our “convert to webp” extension entry in the Nautilus context menu

Creating a “property” extension

Let’s see another example. This time we will create an extension which provides an additional “tab” in the property section of epub files. We will use this space to display some book metadata:

"""
Add a Nautilus property page to epub files to display book metadata.

Author: Egidio Docile
"""

from gi.repository import GObject, Gtk, Nautilus
from urllib.parse import urlparse, unquote

import epub_meta


class EpubMetadataPropertyPageProvider(GObject.GObject, Nautilus.PropertyPageProvider):

    def _get_epub_metadata(self, file):
        file_path = unquote(urlparse(file.get_uri()).path)
        
        return epub_meta.get_epub_metadata(file_path)


    def get_property_pages(self, files):
        if len(files) != 1:
            return

        file = files[0]
        if file.get_mime_type() != 'application/epub+zip':
            return ()

        metadata = self._get_epub_metadata(file)

        property_label = Gtk.Label('Epub Metadata')
        property_label.show()
        
        grid = Gtk.Grid()
        grid.props.margin_left = 50
        grid.props.margin_top = 20
        
        for row, key in enumerate(['authors', 'publisher', 'title']):
            val = ",".join(metadata[key]) if isinstance(metadata[key], list) else metadata[key] 
            grid.attach(Gtk.Label(f'{key.capitalize()}: ', xalign=1), 0, row, 1, 1)
            grid.attach(Gtk.Label(val, xalign=0), 1, row, 1, 1)
        
        grid.show_all()

        page = Nautilus.PropertyPage(
                    name="epub_metadata",
                    label=property_label,
                    page=grid)

        return page,

In this example you can see that from gi.repository we imported also the Gtk module. We did this in order to create a grid container to order and display the ebook metadata.

The get_property_pages method we implemented in the EpubMetadataPropertyPageProvider class, is inherited from the Nautilus.PropertyPageProvider base class. It accepts the list of the FileInfo objects representing the selected files as sole argument and returns the list of PropertyPage objects which should be added to the relevant Nautilus section.



The first thing we did in the method body was to test if only one file has been selected, since we want our extension to work only on one file at a time. If the user selects multiple files, we just return an empty tuple, just like we did in the previous example.

If just one file is selected, we make sure it is an epub by checking its MIME type, and eventually we retrieve its metadata (authors, publisher and title). In order to display the data we created a Gtk.Grid object, and we used Gtk.Label objects to show the text.

Finally, we created a Nautilus.PropertyPage object, and returned a tuple containing it as the only element. Now, when we right click on an epub file, and select “Properties” in the Nautilus context-menu, we should see an additional property tab, “Epub Metadata”:

The additional "epub metadata" property page created by the extension
The additional “Epub Metadata” property page created by the extension

Here I didn’t explained in detail how we created the widget, since I consider this to be an implementation detail (and it should be refined). The important thing, in this context, is to understand the main logic behind the extension creation.

Creating a column extension

In our third and final example, we create a “column” extension. Our extension adds a new column containing file permissions displayed in octal form to the Nautilus “list” view. Here is a possible implementation:

"""
Add a column containing the file permissions in octal notation
to the Nautilus list view.

Author: Egidio Docile
"""

import os
from urllib.parse import unquote, urlparse

from gi.repository import GObject, Nautilus


class OctalPermissionsInfoProvider(GObject.GObject, Nautilus.ColumnProvider, Nautilus.InfoProvider):
    def get_columns(self):
        column = Nautilus.Column(
            name="file_permissions_octal",
            attribute="file_permissions_octal",
            label="Permissions (octal)",
            description="The octal file permissions"
        )

        return column,

    def update_file_info(self, file):
        file_path = unquote(urlparse(file.get_uri()).path)
        octal_permissions = oct(os.stat(file_path).st_mode)[-3:]
        file.add_string_attribute('file_permissions_octal', octal_permissions)

In this example, our extension is represented by the OctalPermissionsInfoProvider class, which as usual, inherits from GObject.GObject, and this time, from other two base classes: Nautilus.ColumnProvider and Nautilus.InfoProvider.

Of the former we implemented the get_columns method which must return the list of the additional columns which should be added to Nautilus. In the method body we created just one column, represented as a Nautilus.Column object and returned it as the sole element of a tuple.



To create the Nautilus.Column object we passed four strings as arguments to its constructor. The first one, name, is used as the column identifier; the second, attribute, is the name of the attribute which should be displayed in the column (the file_permissions_octal attribute is added to the FileInfo object representing the file in the update_file_info method). The third argument, label, is the text displayed to the user as the column header, and the last one, description is the description of the column.

The update_file_info method is inherited from the Nautilus.InfoProvider base class; it takes a single FileInfo object as argument. In our implementation we retrieved the permissions of the file and transformed them in octal form; than, we set the result as the value of the file_permissions_octal attribute we added to the object using its add_string_attribute method.

Here is how the additional column is displayed in the Nautilus “list” view (if the column is not displayed by default you should right-click on the columns header, than check the related entry in the columns menu):

The "Octal Permissions" column created by the extension
The “Octal Permissions” column created by the extension

Conclusions

In this tutorial we learned how to create Nautilus extensions using Python, using the bindings provided by the nautilus-python package. We saw how to install the package in some of the most used Linux distributions, we took a look at the Nautilus API, and we created some extensions as examples. To know more about the Nautilus 3.0 API, you can take a look at the following documentation, while to follow the development of nautilus-pytthon you can follow the project on gitlab.



Comments and Discussions
Linux Forum