blob: 9eb548bc4329beef098652a93b6f7aff868b8ccd [file] [log] [blame]
Andrew Scull5e1ddfa2018-08-14 10:06:54 +01001#!/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 (os.environ.get('DISPLAY') or sys.platform[:3] == 'win' or
47 sys.platform == 'darwin'):
48 inout = file(os.devnull, 'r+')
49 else:
50 # for TTY programs, we need stdin/out
51 inout = None
52
53 # if possible, put the child precess in separate process group,
54 # so keyboard interrupts don't affect child precess as well as
55 # Python
56 setsid = getattr(os, 'setsid', None)
57 if not setsid:
58 setsid = getattr(os, 'setpgrp', None)
59
60 pipe = subprocess.Popen(cmdline, stdin=inout, stdout=inout,
61 stderr=inout, close_fds=closefds,
62 preexec_fn=setsid, startupinfo=startupinfo)
63
64 # It is assumed that this kind of tools (gnome-open, kfmclient,
Andrew Scullcdfcccc2018-10-05 20:58:37 +010065 # exo-open, xdg-open and open for OSX) immediately exit after launching
Andrew Scull5e1ddfa2018-08-14 10:06:54 +010066 # the specific application
67 returncode = pipe.wait()
68 if hasattr(self, 'fixreturncode'):
69 returncode = self.fixreturncode(returncode)
70 return not returncode
71
72 def open(self, filename):
73 if isinstance(filename, basestring):
74 cmdline = self.args + [filename]
75 else:
76 # assume it is a sequence
77 cmdline = self.args + filename
78 try:
79 return self._invoke(cmdline)
80 except OSError:
81 return False
82
83
84# Platform support for Windows
85if sys.platform[:3] == 'win':
86
87 class Start(BaseController):
Andrew Scullcdfcccc2018-10-05 20:58:37 +010088 '''Controller for the win32 start program through os.startfile.'''
Andrew Scull5e1ddfa2018-08-14 10:06:54 +010089
90 def open(self, filename):
91 try:
92 os.startfile(filename)
93 except WindowsError:
94 # [Error 22] No application is associated with the specified
95 # file for this operation: '<URL>'
96 return False
97 else:
98 return True
99
100 _controllers['windows-default'] = Start('start')
101 _open = _controllers['windows-default'].open
102
103
104# Platform support for MacOS
105elif sys.platform == 'darwin':
106 _controllers['open']= Controller('open')
107 _open = _controllers['open'].open
108
109
110# Platform support for Unix
111else:
112
Andrew Walbran16937d02019-10-22 13:54:20 +0100113 try:
114 from commands import getoutput
115 except ImportError:
116 from subprocess import getoutput
Andrew Scull5e1ddfa2018-08-14 10:06:54 +0100117
118 # @WARNING: use the private API of the webbrowser module
119 from webbrowser import _iscommand
120
121 class KfmClient(Controller):
122 '''Controller for the KDE kfmclient program.'''
123
124 def __init__(self, kfmclient='kfmclient'):
125 super(KfmClient, self).__init__(kfmclient, 'exec')
126 self.kde_version = self.detect_kde_version()
127
128 def detect_kde_version(self):
129 kde_version = None
130 try:
Andrew Walbran16937d02019-10-22 13:54:20 +0100131 info = getoutput('kde-config --version')
Andrew Scull5e1ddfa2018-08-14 10:06:54 +0100132
133 for line in info.splitlines():
134 if line.startswith('KDE'):
135 kde_version = line.split(':')[-1].strip()
136 break
137 except (OSError, RuntimeError):
138 pass
139
140 return kde_version
141
142 def fixreturncode(self, returncode):
143 if returncode is not None and self.kde_version > '3.5.4':
144 return returncode
145 else:
146 return os.EX_OK
147
148 def detect_desktop_environment():
149 '''Checks for known desktop environments
150
151 Return the desktop environments name, lowercase (kde, gnome, xfce)
152 or "generic"
153
154 '''
155
156 desktop_environment = 'generic'
157
158 if os.environ.get('KDE_FULL_SESSION') == 'true':
159 desktop_environment = 'kde'
160 elif os.environ.get('GNOME_DESKTOP_SESSION_ID'):
161 desktop_environment = 'gnome'
162 else:
163 try:
Andrew Walbran16937d02019-10-22 13:54:20 +0100164 info = getoutput('xprop -root _DT_SAVE_MODE')
Andrew Scull5e1ddfa2018-08-14 10:06:54 +0100165 if ' = "xfce4"' in info:
166 desktop_environment = 'xfce'
167 except (OSError, RuntimeError):
168 pass
169
170 return desktop_environment
171
172
173 def register_X_controllers():
174 if _iscommand('kfmclient'):
175 _controllers['kde-open'] = KfmClient()
176
177 for command in ('gnome-open', 'exo-open', 'xdg-open'):
178 if _iscommand(command):
179 _controllers[command] = Controller(command)
180
181 def get():
182 controllers_map = {
183 'gnome': 'gnome-open',
184 'kde': 'kde-open',
185 'xfce': 'exo-open',
186 }
187
188 desktop_environment = detect_desktop_environment()
189
190 try:
191 controller_name = controllers_map[desktop_environment]
192 return _controllers[controller_name].open
193
194 except KeyError:
Andrew Walbran16937d02019-10-22 13:54:20 +0100195 if 'xdg-open' in _controllers:
Andrew Scull5e1ddfa2018-08-14 10:06:54 +0100196 return _controllers['xdg-open'].open
197 else:
198 return webbrowser.open
199
200
201 if os.environ.get("DISPLAY"):
202 register_X_controllers()
203 _open = get()
204
205
206def open(filename):
Andrew Scullcdfcccc2018-10-05 20:58:37 +0100207 '''Open a file or a URL in the registered default application.'''
Andrew Scull5e1ddfa2018-08-14 10:06:54 +0100208
209 return _open(filename)