1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4"""Utility for opening a file using the default application in a cross-platform
5manner. Modified from http://code.activestate.com/recipes/511443/.
6"""
7
8__version__ = "1.1x"
9__all__ = ["open"]
10
11import os
12import sys
13import webbrowser
14import subprocess
15
16_controllers = {}
17_open = None
18
19
20class BaseController(object):
21    """Base class for open program controllers."""
22
23    def __init__(self, name):
24        self.name = name
25
26    def open(self, filename):
27        raise NotImplementedError
28
29
30class Controller(BaseController):
31    """Controller for a generic open program."""
32
33    def __init__(self, *args):
34        super(Controller, self).__init__(os.path.basename(args[0]))
35        self.args = list(args)
36
37    def _invoke(self, cmdline):
38        if sys.platform[:3] == "win":
39            closefds = False
40            startupinfo = subprocess.STARTUPINFO()
41            startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
42        else:
43            closefds = True
44            startupinfo = None
45
46        if (
47            os.environ.get("DISPLAY")
48            or sys.platform[:3] == "win"
49            or sys.platform == "darwin"
50        ):
51            inout = file(os.devnull, "r+")
52        else:
53            # for TTY programs, we need stdin/out
54            inout = None
55
56        # if possible, put the child precess in separate process group,
57        # so keyboard interrupts don't affect child precess as well as
58        # Python
59        setsid = getattr(os, "setsid", None)
60        if not setsid:
61            setsid = getattr(os, "setpgrp", None)
62
63        pipe = subprocess.Popen(
64            cmdline,
65            stdin=inout,
66            stdout=inout,
67            stderr=inout,
68            close_fds=closefds,
69            preexec_fn=setsid,
70            startupinfo=startupinfo,
71        )
72
73        # It is assumed that this kind of tools (gnome-open, kfmclient,
74        # exo-open, xdg-open and open for OSX) immediately exit after launching
75        # the specific application
76        returncode = pipe.wait()
77        if hasattr(self, "fixreturncode"):
78            returncode = self.fixreturncode(returncode)
79        return not returncode
80
81    def open(self, filename):
82        if isinstance(filename, basestring):
83            cmdline = self.args + [filename]
84        else:
85            # assume it is a sequence
86            cmdline = self.args + filename
87        try:
88            return self._invoke(cmdline)
89        except OSError:
90            return False
91
92
93# Platform support for Windows
94if sys.platform[:3] == "win":
95
96    class Start(BaseController):
97        """Controller for the win32 start program through os.startfile."""
98
99        def open(self, filename):
100            try:
101                os.startfile(filename)
102            except WindowsError:
103                # [Error 22] No application is associated with the specified
104                # file for this operation: '<URL>'
105                return False
106            else:
107                return True
108
109    _controllers["windows-default"] = Start("start")
110    _open = _controllers["windows-default"].open
111
112
113# Platform support for MacOS
114elif sys.platform == "darwin":
115    _controllers["open"] = Controller("open")
116    _open = _controllers["open"].open
117
118
119# Platform support for Unix
120else:
121
122    try:
123        from commands import getoutput
124    except ImportError:
125        from subprocess import getoutput
126
127    # @WARNING: use the private API of the webbrowser module
128    from webbrowser import _iscommand
129
130    class KfmClient(Controller):
131        """Controller for the KDE kfmclient program."""
132
133        def __init__(self, kfmclient="kfmclient"):
134            super(KfmClient, self).__init__(kfmclient, "exec")
135            self.kde_version = self.detect_kde_version()
136
137        def detect_kde_version(self):
138            kde_version = None
139            try:
140                info = getoutput("kde-config --version")
141
142                for line in info.splitlines():
143                    if line.startswith("KDE"):
144                        kde_version = line.split(":")[-1].strip()
145                        break
146            except (OSError, RuntimeError):
147                pass
148
149            return kde_version
150
151        def fixreturncode(self, returncode):
152            if returncode is not None and self.kde_version > "3.5.4":
153                return returncode
154            else:
155                return os.EX_OK
156
157    def detect_desktop_environment():
158        """Checks for known desktop environments
159
160        Return the desktop environments name, lowercase (kde, gnome, xfce)
161        or "generic"
162
163        """
164
165        desktop_environment = "generic"
166
167        if os.environ.get("KDE_FULL_SESSION") == "true":
168            desktop_environment = "kde"
169        elif os.environ.get("GNOME_DESKTOP_SESSION_ID"):
170            desktop_environment = "gnome"
171        else:
172            try:
173                info = getoutput("xprop -root _DT_SAVE_MODE")
174                if ' = "xfce4"' in info:
175                    desktop_environment = "xfce"
176            except (OSError, RuntimeError):
177                pass
178
179        return desktop_environment
180
181    def register_X_controllers():
182        if _iscommand("kfmclient"):
183            _controllers["kde-open"] = KfmClient()
184
185        for command in ("gnome-open", "exo-open", "xdg-open"):
186            if _iscommand(command):
187                _controllers[command] = Controller(command)
188
189    def get():
190        controllers_map = {
191            "gnome": "gnome-open",
192            "kde": "kde-open",
193            "xfce": "exo-open",
194        }
195
196        desktop_environment = detect_desktop_environment()
197
198        try:
199            controller_name = controllers_map[desktop_environment]
200            return _controllers[controller_name].open
201
202        except KeyError:
203            if "xdg-open" in _controllers:
204                return _controllers["xdg-open"].open
205            else:
206                return webbrowser.open
207
208    if os.environ.get("DISPLAY"):
209        register_X_controllers()
210    _open = get()
211
212
213def open(filename):
214    """Open a file or a URL in the registered default application."""
215
216    return _open(filename)
217