Scripting the Linux desktop, Part 2: Scripting Nautilus

Using Python to expand and extend Nautilus

This series of articles explores how to use Python to create scripts for the GNOME desktop, the screenlets framework, and Nautilus to deliver a highly productive environment. Scripts on the desktop enable drag-and-drop functionality and quick access to the information and services you commonly use. In this installment, learn how to use Python to add functionality to extend Nautilus on your desktop.

Paul Ferrill (paul@ferrill.net), CTO, ATAC

Paul Ferrill has been writing in the computer trade press for more than 20 years. He got his start writing networking reviews for PC Magazine on products like LANtastic and early versions of Novell Netware. Paul holds both BSEE and MSEE degrees and has written software for more computer platforms and architectures than he can remember.



16 February 2011

Also available in Russian Japanese Portuguese

For users of the GNOME desktop, the Nautilus program is probably one of the more frequently used applications. It handles all the file copying, moving, renaming, and searching chores with a simple graphical interface. At first blush, it would appear there aren't many file-related things Nautilus can't do—unless you start thinking about tasks you would typically perform with a shell script.

The Nautilus developers provided several ways to add new functionality without breaking open the main code base. The simplest method is to use a bash or shell script that executes a series of commands you would usually perform from a terminal prompt. This method makes it possible to try the commands to make sure they do what you want them to do first. You can use other languages as well, including the C Scripting Language, GnomeBasic, Perl, and Python. This article looks at adding new capabilities to Nautilus using the Python language. A basic understanding of the Python language and the Python Standard Library is assumed.

Nautilus scripting

The first method for extending Nautilus is through a special directory found in /home named .gnome2/nautilus-scripts. Any executable file placed in this directory will appear when you right-click a file or folder under the Scripts menu. You can also select multiple files or folders and pass a list of files to a script with the same right-click method.

Nautilus makes available a number of environment variables containing things like the current directory and the selected files when a script is invoked. Table 1 shows these environment variables.

Table 1. Nautilus environment variables
Environment variableDescription
NAUTILUS_SCRIPT_SELECTED_FILE_PATHSNewline-delimited paths for selected files (only if local)
NAUTILUS_SCRIPT_SELECTED_URISNewline-delimited URIs for selected files
NAUTILUS_SCRIPT_CURRENT_URIThe current location
NAUTILUS_SCRIPT_WINDOW_GEOMETRYThe position and size of the current window

In Python, you obtain the value of these variables with a single call to the os.environ.get function as follows:

selected = os.environ.get('NAUTILUS_SCRIPT_SELECTED_FILE_PATHS,'')

This call returns a string with paths to all the selected files delimited with the newline character. Python makes it easy to turn this string into an iterable list with the following line:

targets = selected.splitlines()

At this point, it's probably a good idea to stop and talk about user interaction. Once control is passed from Nautilus to the script, there are really no restrictions on what the script does from that point on. Depending on what the script does, there might not even be a need for any user feedback, with the exception of some type of completion or error message, which you can take care of with a simple message box. Because Nautilus is written using the gtk windowing toolkit, it seems like a logical choice to do the same, although this is not required. You could just as easily use TkInter or wxPython.

For the purposes of this article, you'll use gtk. Producing a simple message box to communicate completion status requires just a few lines of code. For readability purposes, this code will fit best if you create a simple function to generate the message. Doing so requires a total of four lines of code:

def alert(msg):
    dialog = gtk.MessageDialog()
    dialog.set_markup(msg)
	dialog.run()

Example: Creating a simple script to return the number of selected files

The first example program combines these snippets into a simple script that returns the number of files currently selected. This script will work for individual files or directories. You'll use another Python library function, os.walk, to recursively build a list of files in each directory. A total of 38 lines of code, shown in Listing 1, is all you needed for this little utility, including blanks lines.

Listing 1. Python code for the Filecount script
#!/usr/bin/env python
import pygtk
pygtk.require('2.0')
import gtk
import os

def alert(msg):
    """Show a dialog with a simple message."""

    dialog = gtk.MessageDialog()
    dialog.set_markup(msg)
    dialog.run()

def main():
    selected = os.environ.get('NAUTILUS_SCRIPT_SELECTED_URIS', '')
    curdir = os.environ.get('NAUTILUS_SCRIPT_CURRENT_URI', os.curdir)
    
    if selected:
        targets = selected.splitlines()
    else:
        targets = [curdir]
    
    files = []
    directories = []
    
    for target in targets:
        if target.startswith('file:///'):
            target = target[7:]
        for dirname, dirnames, filenames in os.walk(target):
            for dirname in dirnames:
                directories.append(dirname)
            for filename in filenames:
                files.append(filename)

    alert('%s directories and %s files' %
          (len(directories),len(files)))

if __name__ == "__main__":
    main()

Figure 1 shows what you should see in Nautilus when you right-click a file or select a group of files. The Scripts menu option displays all executable files in .gnome2/nautilus-scripts and also gives you the option to open that folder. Selecting one of the files executes that script.

Figure 1. Selecting files in Nautilus
Image showing what happens when you select files in Nautilus

Figure 2 shows the output from running the Filecount.py script.

Figure 2. Filecount.py output
Image showing output from running the Filecount.py script

A few things might come in handy as you go about debugging your Nautilus scripts. The first thing is to close all instances of Nautilus to allow it to reload completely and find your new scripts or extensions. You can do so with the command:

nautilus -q

The next handy command allows you to run Nautilus without opening your preference or profile data. This could save you a few steps later on in the event your script or extension inadvertently corrupts something. Here's the command:

nautilus -no-desktop

The only step remaining to make your filecount utility accessible from Nautilus is to copy it to the ~/.gnome2/nautilus-scripts directory and change the file mode to allow execution. You do so with the following command:

chmod +x Filecount.py

Example: Creating a file cleanup utility

As a second example, create a file cleanup utility to look for any files that might be considered temporary generated by editors such as Vim or EMACS. The same concepts could be used to purge a directory of any specific files by simply modifying the check function. This code falls into the silent operation category, meaning it executes and provides no feedback to the user.

The main function of this script looks basically the same as the previous example with a few minor exceptions. This code uses the concept of recursion to call the main function multiple times until the last directory has been traversed. You could use the os.walk function to accomplish the same task without using recursion. File checking occurs in the check function and simply looks for files ending with a tilde (~) or pound sign (#), beginning with a pound sign, or ending with the extension .pyc. This example shows off the extensive number of functions the Python Standard Library os module provides. It's also a good example of an operating system-independent way of manipulating path names and directories as well as performing file operations. Listing 2 shows the code for this script.

Listing 2. Python code for the cleanup script
#!/usr/bin/env python

import pygtk
pygtk.require('2.0')
import gtk
import os

def check(path):
    """Returns true to indicate a file should be removed."""
    
    if path.endswith('~'):
        return True
    if path.startswith('#') and basename.endswith('#'):
        return True
    if path.endswith('.pyc'):
        return True
    return False

def walk(dirname=None):
    selected = os.environ.get('NAUTILUS_SCRIPT_SELECTED_FILE_PATHS', '')
    curdir = os.environ.get('NAUTILUS_SCRIPT_CURRENT_URI', os.curdir)
    
    if dirname is not None:
        targets = [dirname]
    elif selected:
        targets = selected.splitlines()
    else:
        targets = [curdir]
    
    for target in targets:
        if target.startswith('file:///'):
            target = target[7:]
        if not os.path.isdir(target): continue
        for dirname, dirnames, files in os.walk(target):
            for dir in dirnames:
                dir = os.path.join(dirname, dir)
                walk(dir)
            for file in files:
                file = os.path.join(dirname, file)
                if check(file):
                    os.remove(file)

if __name__ == '__main__':
    walk()

Nautilus extensions

The second method for enhancing Nautilus is through the creation of extensions. This method is somewhat more complex than the first but brings added benefits. Nautilus extensions can be embedded in the file display window, so you can write an extension that will populate a column with information previously unavailable. One of the first things you may need to do is install the python-nautilus extensions with the following command:

sudo apt-get install python-nautilus

This command downloads and installs the necessary files, including documentation and examples. You can find the sample code in the directory /usr/share/doc/python-nautilus/examples. Once installed, you have access to a set of Nautilus classes and providers to program against. Table 2 shows the list.

Table 2. Nautilus classes and providers
Class or providerDescription
nautilus.ColumnReference to the Nautilus column object
nautilus.FileInfoReference to the Nautilus fileinfo object
nautilus.MenuReference to the Nautilus menu object
nautilus.MenuItemReference to the Nautilus menuitem object
nautilus.PropertyPageReference to the Nautilus propertypage object
nautilus.ColumnProviderAllows output to be displayed in a Nautilus column
nautilus.InfoProviderProvides information about the file
nautilus.LocationWidgetProviderDisplays the location
nautilus.MenuProviderAdds new functionality to the right-click menu
nautilus.PropertyPageProviderAdds information to the property page

The examples provided on the gnome.org site demonstrate the use of MenuProvider (background-image.py and open-terminal.py), ColumnProvider and InfoProvider (block-size-column.py), and PropertyPageProvider (md5sum-property-page.py). The ColumnProvider uses 13 lines of executable Python code to introduce a new column to Nautilus. Once this code has been placed in the proper directory (~/.nautilus/python-extensions) and Nautilus has restarted, you should see a new option when you click View > Visible Columns. The Visible Columns option only appears when you have set your view type to List. Enabling the Block size column by selecting the check box displays the results of the following Python library call:

str(os.stat(filename).st_blksize))

The basic pattern of any Python extension is to subclass the existing Nautilus provider base class, and then execute a series of instructions that will eventually return the appropriate Nautilus object. In the block-size-column.py example, the object returned is nautilus.Column. You must pass four parameters back to Nautilus, including name, attribute, label, and description. The Python code for this example is:

return nautilus.Column("NautilusPython::block_size_column", 
                       "block_size", 
                       "Block size", 
                       "Get the block size")

Coding a new extension involves inheriting needed information from the specified base classes. In the block-size-column.py example, both nautilus.ColumnProvider and nautilus.InfoProvider are enumerated in the class definition, so the new class inherits from both. The next thing you have to do is override any method from the base class or classes to populate the column. You do so in the block-size-column.py example by overriding the get_columns and update_file_info methods.

Passing information to a Nautilus extension works differently than the scripting case. Nautilus actually launches a new process to execute a script and sets a number of environment variables to pass information. Extensions execute in the same process as Nautilus and, therefore, have access to objects, methods, and attributes. Information about a file gets passed through the nautilus.FileInfo object, including things like file_type, location, name, uri, and mime_type. To add information to the FileInfo object, you must call the add_string_attribute method. You'll use this approach in the example that follows to add new attributes to the FileInfo object.

Example: Show the number of lines in a file

The first example uses the PropertyPageProvider method to show the number of lines and characters when you right-click a file (or files) and then click Properties. The basic idea behind this extension is to count the number of lines and characters in a file and report the results on a new tab of the file properties page. Extensions have direct access to Nautilus data structures, including the file object. The only thing you have to do is unpack the name using the urllib.unquote library function as follows:

filename = urllib.unquote(file.get_uri()[7:]

A few lines of Python code do the main work of counting the number of lines and characters. For this example, you create a count function to read the entire file into one big string, and then count the total number of characters and the number of newline characters. Because the property page can be displayed for a number of selected files and directories, you have to make provisions for counting multiple files. All that's left at this point is to add the results to a new page on the property page. This example creates a simple gtk.Hbox, and then populates a number of labels with the information gathered, as Listing 3 shows.

Listing 3. The Linecountextension.py file
import nautilus
import urllib
import gtk
import os

types = ['.py','.js','.html','.css','.txt','.rst','.cgi']
exceptions = ('MochiKit.js',)

class LineCountPropertyPage(nautilus.PropertyPageProvider):
    def __init__(self):
        pass
    
    def count(self, filename):
        s = open(filename).read()
        return s.count('\n'), len(s)
    
    def get_property_pages(self, files):
        if not len(files):
            return
        
        lines = 0
        chars = 0
        
        for file in files:
            if not file.is_directory():
                result = self.count(urllib.unquote(file.get_uri()[7:]))
                lines += result[0]
                chars += result[1]

        self.property_label = gtk.Label('Linecount')
        self.property_label.show()

        self.hbox = gtk.HBox(0, False)
        self.hbox.show()

        label = gtk.Label('Lines:')
        label.show()
        self.hbox.pack_start(label)

        self.value_label = gtk.Label()
        self.hbox.pack_start(self.value_label)

        self.value_label.set_text(str(lines))
        self.value_label.show()
        
        self.chars_label = gtk.Label('Characters:')
        self.chars_label.show()
        self.hbox.pack_start(self.chars_label)
        
        self.chars_value = gtk.Label()
        self.hbox.pack_start(self.chars_value)
        self.chars_value.set_text(str(chars))
        self.chars_value.show()
        
        return nautilus.PropertyPage("NautilusPython::linecount",
                                     self.property_label, self.hbox),

Figure 3 shows the result of right-clicking a file and clicking the Linecount tab. It's important to note at this point that this feature works on individual files or any group of selected files and directories. The reported number will represent all lines in all files.

Figure 3. Clicking the Linecount tab to view the number of lines in a file
Image showing what happens when you click the Linecount tab to view the number of lines in a file

Finally, alter your extension utility to populate a column instead of a property page. The modifications to the code are fairly minor, although you do need to inherit from both nautilus.ColumnProvider and nautilus.InfoProvider. You also have to implement get_columns and update_file_info. The get_columns method simply returns the information gathered by the count method.

The count method uses a different technique for the column provider extension. Python's readlines routine is used to read all lines in a file into a list of strings. Counting the total number of lines is simply the number of elements in the list returned with the len(s) statement. Not that the file type check is common to both examples: It only makes sense to count text files that actually have lines to count. You create a list of acceptable file extensions with the line:

types = ['.py','.js','.html','.css','.txt','.rst','.cgi']

A second list contains exceptions that won't be counted. For this example, exclude a single file with the line:

exceptions = ['MochiKit.js']

These two lists are then used to include or exclude files with the following two lines of code:

if ext not in types or basename in exceptions:
    return 0

The entire extension requires a total of 26 lines of executable code. You'll want to modify the exceptions and types lists to include or exclude the files of interest to you. Listing 4 shows the completed extension.

Listing 4. Python code for the Linecountcolumn extension
import nautilus
import urllib
import os

types = ['.py','.js','.html','.css','.txt','.rst','.cgi']
exceptions = ['MochiKit.js']

class LineCountExtension(nautilus.ColumnProvider, nautilus.InfoProvider):
    def __init__(self):
        pass
    
    def count(self, filename):
        ext = os.path.splitext(filename)[1]
        basename = os.path.basename(filename)
        if ext not in types or basename in exceptions:
            return 0

        s = open(filename).readlines()
        return len(s)
    
    def get_columns(self):
        return nautilus.Column("NautilusPython::linecount",
                               "linecount",
                               "Line Count",
                               "The number of lines of code"),

    def update_file_info(self, file):
        if file.is_directory():
            lines = 'n/a'
        else:
            lines = self.count(urllib.unquote(file.get_uri()[7:]))
        
        file.add_string_attribute('linecount', str(lines))

Figure 4 shows a Nautilus window with the Line Count column enabled. Each individual file will have the total number of lines displayed. You'll have to do some math using this method should you need to get a total of multiple files.

Figure 4. The Line Count column in a Nautilus window
Image showing the Line Count column in a Nautilus window

Wrapping up

Extending Nautilus with Python is really a straightforward process. The beauty and elegance of Python and the Python Standard Library make for both efficient and readable code. Navigating the documentation and examples on the gnome.org site can be challenging but not impossible. A few Google searches will turn up additional examples, as well. The examples here will hopefully give you ideas of how to extend Nautilus in ways that will meet specific needs. If you're familiar with coding in Python, you shouldn't have any problems.

Resources

Learn

Get products and technologies

  • Evaluate IBM products in the way that suits you best: Download a product trial, try a product online, use a product in a cloud environment, or spend a few hours in the SOA Sandbox learning how to implement Service Oriented Architecture efficiently.

Discuss

  • Get involved in the My developerWorks community. Connect with other developerWorks users while exploring the developer-driven blogs, forums, groups, and wikis.

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Linux on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Linux
ArticleID=627274
ArticleTitle=Scripting the Linux desktop, Part 2: Scripting Nautilus
publish-date=02162011