1 #!/usr/bin/env python 2 3 """ 4 Simple desktop integration for Python. This module provides desktop environment 5 detection and resource opening support for a selection of common and 6 standardised desktop environments. 7 8 Copyright (C) 2005, 2006 Paul Boddie <paul@boddie.org.uk> 9 10 This library is free software; you can redistribute it and/or 11 modify it under the terms of the GNU Lesser General Public 12 License as published by the Free Software Foundation; either 13 version 2.1 of the License, or (at your option) any later version. 14 15 This library is distributed in the hope that it will be useful, 16 but WITHOUT ANY WARRANTY; without even the implied warranty of 17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 18 Lesser General Public License for more details. 19 20 You should have received a copy of the GNU Lesser General Public 21 License along with this library; if not, write to the Free Software 22 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 23 24 -------- 25 26 Desktop Detection 27 ----------------- 28 29 To detect a specific desktop environment, use the get_desktop function. 30 To detect whether the desktop environment is standardised (according to the 31 proposed DESKTOP_LAUNCH standard), use the is_standard function. 32 33 Opening URLs 34 ------------ 35 36 To open a URL in the current desktop environment, relying on the automatic 37 detection of that environment, use the desktop.open function as follows: 38 39 desktop.open("http://www.python.org") 40 41 To override the detected desktop, specify the desktop parameter to the open 42 function as follows: 43 44 desktop.open("http://www.python.org", "KDE") # Insists on KDE 45 desktop.open("http://www.python.org", "GNOME") # Insists on GNOME 46 47 Without overriding using the desktop parameter, the open function will attempt 48 to use the "standard" desktop opening mechanism which is controlled by the 49 DESKTOP_LAUNCH environment variable as described below. 50 51 Opening Dialogue Boxes (Dialogs) 52 -------------------------------- 53 54 To open a dialogue box (dialog) in the current desktop environment, relying on 55 the automatic detection of that environment, use the desktop.dialog function as 56 follows: 57 58 desktop.dialog("question", text="Are you sure?") 59 60 To override the detected desktop, specify the desktop parameter to the dialog 61 function as follows: 62 63 desktop.dialog("question", "KDE", text="Are you sure?") # Insists on KDE 64 desktop.dialog("question", "GNOME", text="Are you sure?") # Insists on GNOME 65 66 The DESKTOP_LAUNCH Environment Variable 67 --------------------------------------- 68 69 The DESKTOP_LAUNCH environment variable must be shell-quoted where appropriate, 70 as shown in some of the following examples: 71 72 DESKTOP_LAUNCH="kdialog --msgbox" Should present any opened URLs in 73 their entirety in a KDE message box. 74 (Command "kdialog" plus parameter.) 75 DESKTOP_LAUNCH="my\ opener" Should run the "my opener" program to 76 open URLs. 77 (Command "my opener", no parameters.) 78 DESKTOP_LAUNCH="my\ opener --url" Should run the "my opener" program to 79 open URLs. 80 (Command "my opener" plus parameter.) 81 82 Details of the DESKTOP_LAUNCH environment variable convention can be found here: 83 http://lists.freedesktop.org/archives/xdg/2004-August/004489.html 84 """ 85 86 __version__ = "0.2.4" 87 88 import os 89 import sys 90 91 try: 92 import subprocess 93 def _run(cmd, shell, wait): 94 opener = subprocess.Popen(cmd, shell=shell) 95 if wait: opener.wait() 96 return opener.pid 97 98 def _readfrom(cmd, shell): 99 opener = subprocess.Popen(cmd, shell=shell, stdin=subprocess.PIPE, stdout=subprocess.PIPE) 100 opener.stdin.close() 101 return opener.stdout.read() 102 103 def _status(cmd, shell): 104 opener = subprocess.Popen(cmd, shell=shell) 105 opener.wait() 106 return opener.returncode == 0 107 108 except ImportError: 109 import popen2 110 def _run(cmd, shell, wait): 111 opener = popen2.Popen3(cmd) 112 if wait: opener.wait() 113 return opener.pid 114 115 def _readfrom(cmd, shell): 116 opener = popen2.Popen3(cmd) 117 opener.tochild.close() 118 opener.childerr.close() 119 return opener.fromchild.read() 120 121 def _status(cmd, shell): 122 opener = popen2.Popen3(cmd) 123 opener.wait() 124 return opener.poll() == 0 125 126 import commands 127 128 # Internal data. 129 130 _dialog_commands = { 131 "KDE" : "kdialog", 132 "GNOME" : "zenity", 133 "X11" : "Xdialog" 134 } 135 136 _dialog_options = { 137 "KDE" : { 138 "question" : (_status, ["--yesno"], ["text"]), 139 "message" : (_status, ["--msgbox"], ["text"]), 140 "input" : (_readfrom, ["--inputbox"], ["text", "input"]), 141 "password" : (_readfrom, ["--password"], ["text"]), 142 "textfile" : (_readfrom, ["--textbox"], ["filename", "width", "height"]), 143 "menu" : (_readfrom, ["--menu"], ["text", "entries2"]), 144 "radiolist" : (_readfrom, ["--radiolist"], ["text", "entries3"]), 145 "checklist" : (_readfrom, ["--checklist"], ["text", "entries3"]), 146 "pulldown" : (_readfrom, ["--combobox"], ["text", "entries1"]), 147 }, 148 "X11" : { 149 "question" : (_status, ["--stdout", "--yesno"], ["text", "height", "width"]), 150 "message" : (_status, ["--stdout", "--msgbox"], ["text", "height", "width"]), 151 "input" : (_readfrom, ["--stdout", "--inputbox"], ["text", "height", "width", "input"]), 152 "password" : (_readfrom, ["--stdout", "--password", "--inputbox"], ["text", "height", "width"]), 153 "textfile" : (_readfrom, ["--stdout", "--textbox"], ["text", "height", "width"]), 154 "menu" : (_readfrom, ["--stdout", "--menubox"], ["text", "height", "width", "menu_height", "entries2"]), 155 "radiolist" : (_readfrom, ["--stdout", "--radiolist"], ["text", "height", "width", "list_height", "entries3"]), 156 "checklist" : (_readfrom, ["--stdout", "--checklist"], ["text", "height", "width", "list_height", "entries3"]), 157 "pulldown" : (_readfrom, ["--stdout", "--combobox"], ["text", "height", "width", "entries1"]), 158 }, 159 } 160 161 _dialog_defaults = { 162 "width" : 40, "height" : 30, "menu_height" : 20, "list_height" : 20 163 } 164 165 # Introspection functions. 166 167 def get_desktop(): 168 169 """ 170 Detect the current desktop environment, returning the name of the 171 environment. If no environment could be detected, None is returned. 172 """ 173 174 if os.environ.has_key("KDE_FULL_SESSION") or \ 175 os.environ.has_key("KDE_MULTIHEAD"): 176 return "KDE" 177 elif os.environ.has_key("GNOME_DESKTOP_SESSION_ID") or \ 178 os.environ.has_key("GNOME_KEYRING_SOCKET"): 179 return "GNOME" 180 elif sys.platform == "darwin": 181 return "Mac OS X" 182 elif hasattr(os, "startfile"): 183 return "Windows" 184 elif os.environ.has_key("DISPLAY"): 185 return "X11" 186 else: 187 return None 188 189 def use_desktop(desktop): 190 191 """ 192 Decide which desktop should be used, based on the detected desktop and a 193 supplied 'desktop' argument (which may be None). Return an identifier 194 indicating the desktop type as being either "standard" or one of the results 195 from the 'get_desktop' function. 196 """ 197 198 # Attempt to detect a desktop environment. 199 200 detected = get_desktop() 201 202 # Start with desktops whose existence can be easily tested. 203 204 if (desktop is None or desktop == "standard") and is_standard(): 205 return "standard" 206 elif (desktop is None or desktop == "Windows") and detected == "Windows": 207 return "Windows" 208 209 # Test for desktops where the overriding is not verified. 210 211 elif (desktop or detected) == "KDE": 212 return "KDE" 213 elif (desktop or detected) == "GNOME": 214 return "GNOME" 215 elif (desktop or detected) == "Mac OS X": 216 return "Mac OS X" 217 elif (desktop or detected) == "X11": 218 return "X11" 219 else: 220 return None 221 222 def is_standard(): 223 224 """ 225 Return whether the current desktop supports standardised application 226 launching. 227 """ 228 229 return os.environ.has_key("DESKTOP_LAUNCH") 230 231 # Activity functions. 232 233 def open(url, desktop=None, wait=0): 234 235 """ 236 Open the 'url' in the current desktop's preferred file browser. If the 237 optional 'desktop' parameter is specified then attempt to use that 238 particular desktop environment's mechanisms to open the 'url' instead of 239 guessing or detecting which environment is being used. 240 241 Suggested values for 'desktop' are "standard", "KDE", "GNOME", "Mac OS X", 242 "Windows" where "standard" employs a DESKTOP_LAUNCH environment variable to 243 open the specified 'url'. DESKTOP_LAUNCH should be a command, possibly 244 followed by arguments, and must have any special characters shell-escaped. 245 246 The process identifier of the "opener" (ie. viewer, editor, browser or 247 program) associated with the 'url' is returned by this function. If the 248 process identifier cannot be determined, None is returned. 249 250 An optional 'wait' parameter is also available for advanced usage and, if 251 'wait' is set to a true value, this function will wait for the launching 252 mechanism to complete before returning (as opposed to immediately returning 253 as is the default behaviour). 254 """ 255 256 # Decide on the desktop environment in use. 257 258 desktop_in_use = use_desktop(desktop) 259 260 if desktop_in_use == "standard": 261 arg = "".join([os.environ["DESKTOP_LAUNCH"], commands.mkarg(url)]) 262 return _run(arg, 1, wait) 263 264 elif desktop_in_use == "Windows": 265 # NOTE: This returns None in current implementations. 266 return os.startfile(url) 267 268 elif desktop_in_use == "KDE": 269 cmd = ["kfmclient", "exec", url] 270 271 elif desktop_in_use == "GNOME": 272 cmd = ["gnome-open", url] 273 274 elif desktop_in_use == "Mac OS X": 275 cmd = ["open", url] 276 277 # Finish with an error where no suitable desktop was identified. 278 279 else: 280 raise OSError, "Desktop not supported (neither DESKTOP_LAUNCH nor os.startfile could be used)" 281 282 return _run(cmd, 0, wait) 283 284 def dialog(dialog_type, desktop=None, **options): 285 286 """ 287 Open a dialogue box (dialog) using a program appropriate to the desktop 288 environment in use. The specified 'dialog_type' may be one of the following: 289 290 question A dialogue asking a question and showing response buttons. 291 Options: text, width (in characters), height (in characters) 292 293 message A message dialogue. 294 Options: text, level ("warning", "error", "info") 295 296 menu A menu of options, one of which being selectable. 297 Options: text, width (in characters), height (in characters), 298 menu_height (in entries), entries (list of (value, text) tuples) 299 300 radiolist A list of radio buttons, one of which being selectable. 301 Options: text, width (in characters), height (in characters), 302 list_height (in entries), entries (list of (value, text, status) 303 tuples) 304 305 checklist A list of checkboxes, many being selectable. 306 Options: text, width (in characters), height (in characters), 307 list_height (in entries), entries (list of (value, text, status) 308 tuples) 309 310 pulldown A pull-down menu of options, one of which being selectable. 311 Options: text, width (in characters), height (in characters), 312 entries (list of values) 313 314 input An input dialogue, consisting of an input field. 315 Options: text, width (in characters), height (in characters), 316 input 317 318 password A password dialogue, consisting of a password entry field. 319 Options: text, width (in characters), height (in characters), 320 input 321 322 textfile A text file input box. 323 Options: text, width (in characters), height (in characters), 324 filename 325 326 If the optional 'desktop' parameter is specified then attempt to use that 327 particular desktop environment's mechanisms to open the 'url' instead of 328 guessing or detecting which environment is being used. 329 330 Suggested values for 'desktop' are "standard", "KDE", "GNOME", "Mac OS X", 331 "Windows". 332 333 The result of the dialogue interaction may be a string indicating user 334 input (for input, password, menu, radiolist, pulldown), a list of strings 335 indicating selections of one or more items (for checklist), or a value 336 indicating true or false (for question). 337 """ 338 339 # Decide on the desktop environment in use. 340 341 desktop_in_use = use_desktop(desktop) 342 343 # Get the base command. 344 345 cmd = _dialog_commands[desktop_in_use] 346 347 # Get the handler for the command, along with required command options and 348 # the fields that the command expects. 349 350 handler, cmd_options, fields = _dialog_options[desktop_in_use][dialog_type] 351 352 # Form a command list. 353 354 l = [cmd] + cmd_options 355 356 # Process the fields, adding values if found or defaults if available. 357 358 for field in fields: 359 if field[-1].isdigit(): 360 n = int(field[-1]) 361 field = field[:-1] 362 else: 363 n = 0 # not list 364 365 if options.has_key(field): 366 if n == 0: 367 l.append(str(options[field])) 368 else: 369 values = options[field] 370 for value in values: 371 if n == 1: 372 l.append(str(value)) 373 else: 374 for i in range(0, n): 375 l.append(str(value[i])) 376 elif _dialog_defaults.has_key(field): 377 l.append(str(_dialog_defaults[field])) 378 else: 379 raise ValueError, "Field '%s' missing from options." % field 380 381 print l 382 return handler(l, 0) 383 384 # vim: tabstop=4 expandtab shiftwidth=4