# HG changeset patch # User paulb # Date 1196642235 0 # Node ID d7f51463d6116eb1c0f21740382d2b554f876de8 # Parent 7cea22629e6b14795c6b7da2f6f417c16c99dda4 [project @ 2007-12-03 00:37:15 by paulb] Converted the desktop module into a package. Added dialog and windows modules. diff -r 7cea22629e6b -r d7f51463d611 desktop/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/desktop/__init__.py Mon Dec 03 00:37:15 2007 +0000 @@ -0,0 +1,276 @@ +#!/usr/bin/env python + +""" +Simple desktop integration for Python. This module provides desktop environment +detection and resource opening support for a selection of common and +standardised desktop environments. + +Copyright (C) 2005, 2006, 2007 Paul Boddie + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +-------- + +Desktop Detection +----------------- + +To detect a specific desktop environment, use the get_desktop function. +To detect whether the desktop environment is standardised (according to the +proposed DESKTOP_LAUNCH standard), use the is_standard function. + +Opening URLs +------------ + +To open a URL in the current desktop environment, relying on the automatic +detection of that environment, use the desktop.open function as follows: + +desktop.open("http://www.python.org") + +To override the detected desktop, specify the desktop parameter to the open +function as follows: + +desktop.open("http://www.python.org", "KDE") # Insists on KDE +desktop.open("http://www.python.org", "GNOME") # Insists on GNOME + +Without overriding using the desktop parameter, the open function will attempt +to use the "standard" desktop opening mechanism which is controlled by the +DESKTOP_LAUNCH environment variable as described below. + +The DESKTOP_LAUNCH Environment Variable +--------------------------------------- + +The DESKTOP_LAUNCH environment variable must be shell-quoted where appropriate, +as shown in some of the following examples: + +DESKTOP_LAUNCH="kdialog --msgbox" Should present any opened URLs in + their entirety in a KDE message box. + (Command "kdialog" plus parameter.) +DESKTOP_LAUNCH="my\ opener" Should run the "my opener" program to + open URLs. + (Command "my opener", no parameters.) +DESKTOP_LAUNCH="my\ opener --url" Should run the "my opener" program to + open URLs. + (Command "my opener" plus parameter.) + +Details of the DESKTOP_LAUNCH environment variable convention can be found here: +http://lists.freedesktop.org/archives/xdg/2004-August/004489.html +""" + +__version__ = "0.3" + +import os +import sys + +# Provide suitable process creation functions. + +try: + import subprocess + def _run(cmd, shell, wait): + opener = subprocess.Popen(cmd, shell=shell) + if wait: opener.wait() + return opener.pid + + def _readfrom(cmd, shell): + opener = subprocess.Popen(cmd, shell=shell, stdin=subprocess.PIPE, stdout=subprocess.PIPE) + opener.stdin.close() + return opener.stdout.read() + + def _status(cmd, shell): + opener = subprocess.Popen(cmd, shell=shell) + opener.wait() + return opener.returncode == 0 + +except ImportError: + import popen2 + def _run(cmd, shell, wait): + opener = popen2.Popen3(cmd) + if wait: opener.wait() + return opener.pid + + def _readfrom(cmd, shell): + opener = popen2.Popen3(cmd) + opener.tochild.close() + opener.childerr.close() + return opener.fromchild.read() + + def _status(cmd, shell): + opener = popen2.Popen3(cmd) + opener.wait() + return opener.poll() == 0 + +import commands + +# Private functions. + +def _get_x11_vars(): + + "Return suitable environment definitions for X11." + + if not os.environ.get("DISPLAY", "").strip(): + return "DISPLAY=:0.0 " + else: + return "" + +def _is_xfce(): + + "Return whether XFCE is in use." + + # XFCE detection involves testing the output of a program. + + try: + return _readfrom(_get_x11_vars() + "xprop -root _DT_SAVE_MODE", shell=1).strip().endswith(' = "xfce4"') + except OSError: + return 0 + +def _is_x11(): + + "Return whether the X Window System is in use." + + return os.environ.has_key("DISPLAY") + +# Introspection functions. + +def get_desktop(): + + """ + Detect the current desktop environment, returning the name of the + environment. If no environment could be detected, None is returned. + """ + + if os.environ.has_key("KDE_FULL_SESSION") or \ + os.environ.has_key("KDE_MULTIHEAD"): + return "KDE" + elif os.environ.has_key("GNOME_DESKTOP_SESSION_ID") or \ + os.environ.has_key("GNOME_KEYRING_SOCKET"): + return "GNOME" + elif sys.platform == "darwin": + return "Mac OS X" + elif hasattr(os, "startfile"): + return "Windows" + elif _is_xfce(): + return "XFCE" + + # KDE, GNOME and XFCE run on X11, so we have to test for X11 last. + + if _is_x11(): + return "X11" + else: + return None + +def use_desktop(desktop): + + """ + Decide which desktop should be used, based on the detected desktop and a + supplied 'desktop' argument (which may be None). Return an identifier + indicating the desktop type as being either "standard" or one of the results + from the 'get_desktop' function. + """ + + # Attempt to detect a desktop environment. + + detected = get_desktop() + + # Start with desktops whose existence can be easily tested. + + if (desktop is None or desktop == "standard") and is_standard(): + return "standard" + elif (desktop is None or desktop == "Windows") and detected == "Windows": + return "Windows" + + # Test for desktops where the overriding is not verified. + + elif (desktop or detected) == "KDE": + return "KDE" + elif (desktop or detected) == "GNOME": + return "GNOME" + elif (desktop or detected) == "XFCE": + return "XFCE" + elif (desktop or detected) == "Mac OS X": + return "Mac OS X" + elif (desktop or detected) == "X11": + return "X11" + else: + return None + +def is_standard(): + + """ + Return whether the current desktop supports standardised application + launching. + """ + + return os.environ.has_key("DESKTOP_LAUNCH") + +# Activity functions. + +def open(url, desktop=None, wait=0): + + """ + Open the 'url' in the current desktop's preferred file browser. If the + optional 'desktop' parameter is specified then attempt to use that + particular desktop environment's mechanisms to open the 'url' instead of + guessing or detecting which environment is being used. + + Suggested values for 'desktop' are "standard", "KDE", "GNOME", "XFCE", + "Mac OS X", "Windows" where "standard" employs a DESKTOP_LAUNCH environment + variable to open the specified 'url'. DESKTOP_LAUNCH should be a command, + possibly followed by arguments, and must have any special characters + shell-escaped. + + The process identifier of the "opener" (ie. viewer, editor, browser or + program) associated with the 'url' is returned by this function. If the + process identifier cannot be determined, None is returned. + + An optional 'wait' parameter is also available for advanced usage and, if + 'wait' is set to a true value, this function will wait for the launching + mechanism to complete before returning (as opposed to immediately returning + as is the default behaviour). + """ + + # Decide on the desktop environment in use. + + desktop_in_use = use_desktop(desktop) + + if desktop_in_use == "standard": + arg = "".join([os.environ["DESKTOP_LAUNCH"], commands.mkarg(url)]) + return _run(arg, 1, wait) + + elif desktop_in_use == "Windows": + # NOTE: This returns None in current implementations. + return os.startfile(url) + + elif desktop_in_use == "KDE": + cmd = ["kfmclient", "exec", url] + + elif desktop_in_use == "GNOME": + cmd = ["gnome-open", url] + + elif desktop_in_use == "XFCE": + cmd = ["exo-open", url] + + elif desktop_in_use == "Mac OS X": + cmd = ["open", url] + + elif desktop_in_use == "X11" and os.environ.has_key("BROWSER"): + cmd = [os.environ["BROWSER"], url] + + # Finish with an error where no suitable desktop was identified. + + else: + raise OSError, "Desktop '%s' not supported (neither DESKTOP_LAUNCH nor os.startfile could be used)" % desktop_in_use + + return _run(cmd, 0, wait) + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r 7cea22629e6b -r d7f51463d611 desktop/dialog.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/desktop/dialog.py Mon Dec 03 00:37:15 2007 +0000 @@ -0,0 +1,422 @@ +#!/usr/bin/env python + +""" +Simple desktop dialogue box support for Python. + +Copyright (C) 2005, 2006, 2007 Paul Boddie + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +-------- + +Opening Dialogue Boxes (Dialogs) +-------------------------------- + +To open a dialogue box (dialog) in the current desktop environment, relying on +the automatic detection of that environment, use the appropriate dialogue box +class: + +question = desktop.dialog.Question("Are you sure?") +question.open() + +To override the detected desktop, specify the desktop parameter to the open +function as follows: + +question.open("KDE") # Insists on KDE +question.open("GNOME") # Insists on GNOME + +The dialogue box options are documented in each class's docstring. +""" + +from desktop import use_desktop, _run, _readfrom, _status + +# Dialogue parameter classes. + +class String: + + "A generic parameter." + + def __init__(self, name): + self.name = name + + def convert(self, value, program): + return [value or ""] + +class Strings(String): + + "Multiple string parameters." + + def convert(self, value, program): + return value or [] + +class StringKeyword: + + "A keyword parameter." + + def __init__(self, keyword, name): + self.keyword = keyword + self.name = name + + def convert(self, value, program): + return [self.keyword + "=" + (value or "")] + +class StringKeywords: + + "Multiple keyword parameters." + + def __init__(self, keyword, name): + self.keyword = keyword + self.name = name + + def convert(self, value, program): + l = [] + for v in value or []: + l.append(self.keyword + "=" + v) + return l + +class Integer(String): + + "An integer parameter." + + defaults = { + "width" : 40, + "height" : 15, + "list_height" : 10 + } + + def convert(self, value, program): + if value is None: + value = self.defaults[self.name] + return [str(int(value))] + +class IntegerKeyword(Integer): + + "An integer keyword parameter." + + def __init__(self, keyword, name): + self.keyword = keyword + self.name = name + + def convert(self, value, program): + if value is None: + value = self.defaults[self.name] + return [self.keyword + "=" + str(int(value))] + +class Boolean(String): + + "A boolean parameter." + + values = { + "kdialog" : ["off", "on"], + "zenity" : ["FALSE", "TRUE"], + "Xdialog" : ["off", "on"] + } + + def convert(self, value, program): + values = self.values[program] + if value: + return [values[1]] + else: + return [values[0]] + +class MenuItemList(String): + + "A menu item list parameter." + + def convert(self, value, program): + l = [] + for v in value: + l.append(v.value) + l.append(v.text) + return l + +class ListItemList(String): + + "A menu item list parameter." + + def convert(self, value, program): + l = [] + for v in value: + l.append(v.value) + l.append(v.text) + boolean = Boolean(None) + l.append(boolean.convert(v.status, program)) + return l + +# Dialogue argument values. + +class MenuItem: + def __init__(self, value, text): + self.value = value + self.text = text + +class ListItem(MenuItem): + def __init__(self, value, text, status): + MenuItem.__init__(self, value, text) + self.status = status + +# Dialogue classes. + +class Dialogue: + + commands = { + "KDE" : "kdialog", + "GNOME" : "zenity", + "X11" : "Xdialog" + } + + def open(self, desktop=None): + + """ + Open a dialogue box (dialog) using a program appropriate to the desktop + environment in use. + + If the optional 'desktop' parameter is specified then attempt to use that + particular desktop environment's mechanisms to open the dialog instead of + guessing or detecting which environment is being used. + + Suggested values for 'desktop' are "standard", "KDE", "GNOME", "Mac OS X", + "Windows". + + The result of the dialogue interaction may be a string indicating user + input (for input, password, menu, radiolist, pulldown), a list of strings + indicating selections of one or more items (for checklist), or a value + indicating true or false (for question). + """ + + # Decide on the desktop environment in use. + + desktop_in_use = use_desktop(desktop) + + # Get the program. + + try: + program = self.commands[desktop_in_use] + except KeyError: + raise OSError, "Desktop '%s' not supported (no known dialogue box command could be suggested)" % desktop_in_use + + handler, options = self.info[program] + + cmd = [program] + for option in options: + if isinstance(option, str): + cmd.append(option) + else: + value = getattr(self, option.name, None) + cmd += option.convert(value, program) + + print cmd + return handler(cmd, 0) + +class Simple(Dialogue): + def __init__(self, text, width=None, height=None): + self.text = text + self.width = width + self.height = height + +class Question(Simple): + + """ + A dialogue asking a question and showing response buttons. + Options: text, width (in characters), height (in characters) + """ + + name = "question" + info = { + "kdialog" : (_status, ["--yesno", String("text")]), + "zenity" : (_status, ["--question", StringKeyword("--text", "text")]), + "Xdialog" : (_status, ["--stdout", "--yesno", String("text"), Integer("height"), Integer("width")]), + } + +class Warning(Simple): + + """ + A dialogue asking a question and showing response buttons. + Options: text, width (in characters), height (in characters) + """ + + name = "warning" + info = { + "kdialog" : (_status, ["--warningyesno", String("text")]), + "zenity" : (_status, ["--warning", StringKeyword("--text", "text")]), + "Xdialog" : (_status, ["--stdout", "--yesno", String("text"), Integer("height"), Integer("width")]), + } + +class Message(Simple): + + """ + A message dialogue. + Options: text, width (in characters), height (in characters) + """ + + name = "message" + info = { + "kdialog" : (_status, ["--msgbox", String("text")]), + "zenity" : (_status, ["--info", StringKeyword("--text", "text")]), + "Xdialog" : (_status, ["--stdout", "--msgbox", String("text"), Integer("height"), Integer("width")]), + } + +class Error(Simple): + + """ + An error dialogue. + Options: text, width (in characters), height (in characters) + """ + + name = "error" + info = { + "kdialog" : (_status, ["--error", String("text")]), + "zenity" : (_status, ["--error", StringKeyword("--text", "text")]), + "Xdialog" : (_status, ["--stdout", "--msgbox", String("text"), Integer("height"), Integer("width")]), + } + +class Menu(Simple): + + """ + A menu of options, one of which being selectable. + Options: text, width (in characters), height (in characters), + list_height (in items), items (MenuItem objects) + """ + + name = "menu" + info = { + "kdialog" : (_readfrom, ["--menu", String("text"), MenuItemList("items")]), + "zenity" : (_readfrom, ["--list", StringKeyword("--text", "text"), StringKeywords("--column", "titles"), + MenuItemList("items")] + ), + "Xdialog" : (_readfrom, ["--stdout", "--menubox", + String("text"), Integer("height"), Integer("width"), Integer("list_height"), MenuItemList("items")] + ), + } + + def __init__(self, text, titles, items, width=None, height=None, list_height=None): + Simple.__init__(self, text, width, height) + self.titles = titles + self.items = items + self.list_height = list_height + +class RadioList(Menu): + + """ + A list of radio buttons, one of which being selectable. + Options: text, width (in characters), height (in characters), + list_height (in items), items (ListItem objects), titles + """ + + name = "radiolist" + info = { + "kdialog" : (_readfrom, ["--radiolist", String("text"), ListItemList("items")]), + "zenity" : (_readfrom, + ["--list", "--radiolist", StringKeyword("--text", "text"), StringKeywords("--column", "titles"), + ListItemList("items")] + ), + "Xdialog" : (_readfrom, ["--stdout", "--radiolist", + String("text"), Integer("height"), Integer("width"), Integer("list_height"), ListItemList("items")] + ), + } + +class CheckList(Menu): + + """ + A list of checkboxes, many being selectable. + Options: text, width (in characters), height (in characters), + list_height (in items), items (ListItem objects), titles + """ + + name = "checklist" + info = { + "kdialog" : (_readfrom, ["--checklist", String("text"), ListItemList("items")]), + "zenity" : (_readfrom, + ["--list", "--checklist", StringKeyword("--text", "text"), StringKeywords("--column", "titles"), + ListItemList("items")] + ), + "Xdialog" : (_readfrom, ["--stdout", "--checklist", + String("text"), Integer("height"), Integer("width"), Integer("list_height"), ListItemList("items")] + ), + } + +class Pulldown(Menu): + + """ + A pull-down menu of options, one of which being selectable. + Options: text, width (in characters), height (in characters), + entries (list of values) + """ + + name = "pulldown" + info = { + "kdialog" : (_readfrom, ["--combobox", String("text"), Strings("items")]), + "zenity" : (_readfrom, ["--list", "--radiolist", StringKeyword("--text", "text"), StringKeywords("--column", "titles"), + Strings("items")] + ), + "Xdialog" : (_readfrom, ["--stdout", "--combobox", String("text"), Integer("height"), Integer("width"), Strings("items")]), + } + +class Input(Simple): + + """ + An input dialogue, consisting of an input field. + Options: text, width (in characters), height (in characters), + input + """ + + name = "input" + info = { + "kdialog" : (_readfrom, ["--inputbox", String("text"), String("data")]), + "zenity" : (_readfrom, ["--entry", StringKeyword("--text", "text"), StringKeyword("--entry-text", "data")]), + "Xdialog" : (_readfrom, ["--stdout", "--inputbox", String("text"), Integer("height"), Integer("width"), String("data")]), + } + + def __init__(self, text, data, width=None, height=None): + Simple.__init__(self, text, width, height) + self.data = data + +class Password(Input): + + """ + A password dialogue, consisting of a password entry field. + Options: text, width (in characters), height (in characters), + input + """ + + name = "password" + info = { + "kdialog" : (_readfrom, ["--password", String("text")]), + "zenity" : (_readfrom, ["--password", StringKeyword("--text", "text"), "--hide-text"]), + "Xdialog" : (_readfrom, ["--stdout", "--password", "--inputbox", String("text"), Integer("height"), Integer("width")]), + } + +class TextFile(Simple): + + """ + A text file input box. + Options: text, width (in characters), height (in characters), + filename + """ + + name = "textfile" + info = { + "kdialog" : (_readfrom, ["--textbox", String("filename"), String("width"), String("height")]), + "zenity" : (_readfrom, ["--textbox", StringKeyword("--filename", "filename"), IntegerKeyword("--width", "width"), + IntegerKeyword("--height", "height")] + ), + "Xdialog" : (_readfrom, ["--stdout", "--textbox", String("text"), Integer("height"), Integer("width")]), + } + + def __init__(self, text, filename, width=None, height=None): + Simple.__init__(self, text, width, height) + self.filename = filename + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r 7cea22629e6b -r d7f51463d611 desktop/windows.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/desktop/windows.py Mon Dec 03 00:37:15 2007 +0000 @@ -0,0 +1,59 @@ +#!/usr/bin/env python + +""" +Simple desktop window enumeration for Python. + +Copyright (C) 2005, 2006, 2007 Paul Boddie + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +-------- + +Finding Open Windows on the Desktop +----------------------------------- + +To obtain a list of windows, use the desktop.windows.list function as follows: + +desktop.windows.list() +""" + +from desktop import _is_x11, _get_x11_vars, _readfrom, use_desktop + +def list(desktop=None): + + """ + Return a list of window handles for the current desktop. If the optional + 'desktop' parameter is specified then attempt to use that particular desktop + environment's mechanisms to look for windows. + """ + + # NOTE: The desktop parameter is currently ignored and X11 is tested for + # NOTE: directly. + + if _is_x11(): + s = _readfrom(_get_x11_vars() + "xlsclients -a -l", shell=1) + prefix = "Window " + prefix_end = len(prefix) + handles = [] + + for line in s.split("\n"): + if line.startswith(prefix): + handles.append(line[prefix_end:-1]) # NOTE: Assume ":" at end. + else: + raise OSError, "Desktop '%s' not supported" % use_desktop(desktop) + + return handles + +# vim: tabstop=4 expandtab shiftwidth=4