talideon.com

Blackout Ireland

April 7, 2008 at 11:48AM From my ~/bin: talideon-wallpaper

Actually, it’s now ~/.local/bin, but that’s beside the point.

With all the problems I’ve been having with GNOME and FreeBSD 7--though now I’m pretty sure the root of the problem is GnomeVFS and possibly its (currently partial) replacement, GVFS--I switched back to using ROX and gave Openbox a try as a Metacity replacement. So far, I’m quite happy.

I played around with pipemenus this weekend, wrote a wee script for wallpaper management. Sure, there’s plenty of them already, but none of them did quite what I wanted and it seemed like a decent way to experiment with Openbox.

I haven’t checked to see what version of Python this requires, but it should be find with 2.2 or possibly even with 2.1. All you’ll need in addition is ElementTree (either the Python or C versions--it’ll try to go for the right one), and possibly hsetroot for setting the root window, which is on the fallback list. If you’ve some other preferred mechanism, you can list it in the ~/.config/talideon.com/wallpaper/setters file, which, if present, gives a list of commands to try in turn to set the wallpaper. The filename will be appended to the end of whatever command appears to be available.

Usage is simple: if you pass a directory as the first argument, it’ll generate a pipemenu for that directory; if you pass a filename as the first argument, it’ll use that as the wallpaper and record your selection; and if you pass nothing to it, it’ll use whatever the current selection is as the wallpaper.

There’s plenty of places where I think it can be improved, such as proper XDG support, but I’m quite happy with it as it is.

#!/usr/bin/env python
#
# talideon-wallpaper
# by Keith Gaughan <http://talideon.com/>
#
# A pipemenu script for Openbox for selecting and setting the current
# wallpaper.
#
# Copyright (c) Keith Gaughan, 2008.
# All Rights Reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
#  1. Redistributions of source code must retain the above copyright
#     notice, this list of conditions and the following disclaimer.
#
#  2. Redistributions in binary form must reproduce the above copyright
#     notice, this list of conditions and the following disclaimer in the
#     documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS "AS IS" AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# This license is subject to the laws and courts of the Republic of Ireland.
#

import os, sys
from os import path
from sys import argv
try:
    from xml.etree import cElementTree as et
except:
    from xml.etree import ElementTree as et


FALLBACK_SETTERS = [
    "hsetroot -center"
]
SETTINGS = path.expanduser("~/.config/talideon.com/wallpaper")
WP_LINK = path.join(SETTINGS, "current")


def which(name):
    """Finds the path of some named executable available on PATH."""
    env_path = os.environ.get('PATH')
    if env_path:
        for dirname in env_path.split(os.pathsep):
            executable = path.join(dirname, name)
            if path.isfile(executable):
                return executable
    return None


def get_setters():
    """Loads the list of all wallpaper setting commands."""
    setters = []
    setters_path = path.join(SETTINGS, 'setters')
    if path.isfile(setters_path):
        try:
            handle = open(setters_path, 'r')
            for line in handle:
                line = line.strip()
                if line != '' and line[0] != '#':
                    setters.append(line)
        finally:
            handle.close()
    setters.extend(FALLBACK_SETTERS)
    return setters


def show_wallpaper():
    """Gets a wallpaper setter to set the backdrop on the root window."""
    for setter in get_setters():
        parts = setter.split(' ', 2)
        location = which(parts[0])
        if location is not None:
            parts[0] = location
            parts.append(path.realpath(WP_LINK))
            os.spawnvpe(os.P_NOWAIT, location, parts, os.environ)
            return


def set_wallpaper(wallpaper):
    """Records the user's wallpaper selection."""
    if path.isfile(wallpaper) and wallpaper != WP_LINK:
        os.unlink(WP_LINK)
        os.symlink(wallpaper, WP_LINK)


def generate_directory_menu(dirname, tree):
    """Generates the Openbox menuitems for a given directory."""
    for name in sorted(os.listdir(dirname)):
        full_path = path.join(dirname, name)
        if path.isfile(full_path):
            tree.start('item', { 'label': name[:name.rfind('.')] })
            tree.start('action', { 'name': 'Execute' })
            tree.start('execute')
            tree.data("%s '%s'" % (argv[0], full_path))
            tree.end('execute')
            tree.end('action')
            tree.end('item')
        # Unfortunately, submenus, either as pipemenus or directly embedded
        # in a pipemenu, aren't allowed, but we'll leave this in anyway as
        # if/when it does, it'll be a nice hidden bonus feature.
        elif path.isdir(full_path):
            tree.start('menu', { 'label': name })
            generate_directory_menu(full_path, tree)
            tree.end('menu')


def generate_pipemenu(dirname):
    """Generates a pipemenu for a given directory."""
    tree = et.TreeBuilder()
    tree.start('openbox_pipe_menu')
    generate_directory_menu(dirname, tree)
    return et.tostring(tree.close(), 'UTF-8')


def bad_path():
    """
    Generates a pipemenu indicating that the path given was not a valid one.
    """
    tree = et.TreeBuilder()
    tree.start('openbox_pipe_menu')
    tree.start('item', { 'label': 'Bad path' })
    return et.tostring(tree.close(), 'UTF-8')


if __name__ == '__main__':
    if not path.exists(SETTINGS):
        os.makedirs(SETTINGS)
    if len(argv) == 1:
        # With no args, the currently selected wallpaper is rendered.
        show_wallpaper()
    elif len(argv) >= 2:
        if path.isdir(argv[1]):
            # With one arg that' a directory, a pipemenu of that directory's
            # contents is generated.
            print generate_pipemenu(argv[1])
        elif path.isfile(argv[1]):
            # With one arg that's a file, that file is make the current
            # wallpaper.
            set_wallpaper(path.realpath(argv[1]))
            show_wallpaper()
        else:
            print bad_path()
            print >> sys.stderr, "Bad path."

# ex:et sts=4 ts=4 sw=4:

One annoying limitation of pipemenus it that they can’t have submenus, so it’s not possible for me to, say, implement the FreeDesktop.org desktop menu specification. I might look into writing a patch for Openbox to allow it as I’ve taken a look at its source and it appears quite well-written, but that’s way at the end of my to-do list.