Updated to work with XFCE 4.12, will not work with XFCE 4.10 anymore
[bg-scripts] / wallchanger.py
1 #!/usr/bin/env python3
2
3 # Copyright 2008 Greg Darke <greg@tsukasa.net.au>
4 # Copyright 2008 James Bunton <jamesbunton@fastmail.fm>
5 # Licensed for distribution under the GPL version 2, check COPYING for details
6 # This is a cross platform/cross window manager way to change your wallpaper
7
8 import subprocess, sys, os, os.path, time
9 import logging
10 try:
11 import PIL, PIL.Image
12 except ImportError:
13 PIL = None
14
15 __all__ = ("init", "set_image")
16
17
18 changers = []
19
20 def set_image(filename):
21 logging.info("Setting image: %s", filename)
22 for changer in changers:
23 if not changer.set_image(filename):
24 logging.warning("Failed to set background: wallchanger.set_image(%s), changer=%s", filename, changer)
25
26 def get_displays():
27 for line in subprocess.check_output("xrandr").decode("utf-8").split("\n"):
28 tokens = line.split()
29 if len(tokens) < 2 or tokens[1] != "connected":
30 continue
31 yield tokens[0]
32
33 def call_shell(cmd):
34 return subprocess.getstatusoutput(cmd)[0] == 0
35
36 def init(*args, **kwargs):
37 """Desktop Changer factory"""
38
39 classes = []
40
41 if sys.platform == "win32":
42 classes.append(WIN32Changer)
43 return
44
45 logging.debug("Testing for OSX (NonX11)")
46 if call_shell("ps ax -o command -c|grep -q WindowServer"):
47 classes.append(OSXChanger)
48
49 if 'DISPLAY' not in os.environ or os.environ['DISPLAY'].startswith('/tmp/launch'):
50 # X11 is not running
51 return
52 else:
53 if os.uname()[0] == 'Darwin':
54 # Try to detect if the X11 server is running on OSX
55 if call_shell("ps ax -o command|grep -q '^/.*X11 .* %s'" % os.environ['DISPLAY']):
56 # X11 is not running for this display
57 return
58
59 logging.debug("Testing for XFCE4")
60 if call_shell("xwininfo -name 'xfdesktop'"):
61 classes.append(Xfce4Changer)
62
63 logging.debug("Testing for Gnome")
64 if call_shell("xwininfo -name 'gnome-settings-daemon'"):
65 if call_shell("gsettings get org.gnome.desktop.background picture-uri"):
66 classes.append(Gnome3Changer)
67 else:
68 classes.append(Gnome2Changer)
69
70 logging.debug("Testing for xloadimage")
71 if call_shell("which xloadimage"):
72 classes.append(XLoadImageChanger)
73
74 if len(classes) == 0:
75 raise Exception("Unknown window manager")
76
77 for klass in classes:
78 changers.append(klass(*args, **kwargs))
79
80
81 class BaseChanger(object):
82 name = "undefined"
83 def __init__(self, background_color='black', convert=False):
84 logging.info('Determined the window manager is "%s"', self.name)
85 self.background_color = background_color
86 self.convert = convert
87
88 def set_image(self, filename):
89 raise NotImplementedError()
90
91 def convert_image_format(self, filename, format='BMP', allowAlpha=False, extension='.bmp'):
92 """Convert the image to another format, and store it in a local place"""
93 if not os.path.exists(filename):
94 logger.warn('The input file "%s" does not exist, so it will not be converted', filename)
95 return filename, False
96 if PIL is None:
97 logger.warn('PIL could not be found, not converting image format')
98 return filename, False
99
100 self.remove_old_image_cache()
101 output_name = os.path.join(self._ConvertedWallpaperLocation, '%s%s' % (time.time(), extension))
102 img = PIL.Image.open(filename)
103
104 # Remove the alpha channel if the user doens't want it
105 if not allowAlpha and img.mode == 'RGBA':
106 img = img.convert('RGB')
107 img.save(output_name, format)
108
109 return output_name, True
110
111
112 class XLoadImageChanger(BaseChanger):
113 name = "xloadimage"
114 _ConvertedWallpaperLocation = '/tmp/wallpapers_xloadimage/'
115 def remove_old_image_cache(self):
116 """Cleans up any old temp images"""
117 if not os.path.isdir(self._ConvertedWallpaperLocation):
118 os.mkdir(self._ConvertedWallpaperLocation)
119 for fullpath, filenames, dirnames in os.walk(self._ConvertedWallpaperLocation, topdown=False):
120 for filename in filenames:
121 os.unlink(os.path.join(fullpath, filename))
122 for dirname in dirnames:
123 os.unlink(os.path.join(fullpath, dirname))
124
125 def convert_image_format(self, filename):
126 """Convert the image to a png, and store it in a local place"""
127 self.remove_old_image_cache()
128 output_name = os.path.join(self._ConvertedWallpaperLocation, '%s.png' % time.time())
129 cmd = ["convert", filename, output_name]
130 logging.debug("""Convert command: '"%s"'""", '" "'.join(cmd))
131 return output_name, subprocess.call(cmd) == 0
132
133 def set_image(self, filename):
134 if self.convert:
135 filename, convert_status = self.convert_image_format(filename)
136 if convert_status:
137 logging.debug('Convert failed')
138 cmd = [
139 "xloadimage",
140 "-onroot",
141 "-fullscreen",
142 "-border", "black",
143 filename,
144 ]
145 logging.debug('''WMaker bgset command: "'%s'"''', "' '".join(cmd))
146 return subprocess.call(cmd) == 0
147
148 class OSXChanger(BaseChanger):
149 name = "Mac OS X"
150 _ConvertedWallpaperLocation = '/tmp/wallpapers/'
151 _DesktopPlistLocation = os.path.expanduser('~/Library/Preferences/com.apple.desktop.plist')
152
153 def __init__(self, *args, **kwargs):
154 BaseChanger.__init__(self, *args, **kwargs)
155
156 def remove_old_image_cache(self):
157 """Cleans up any old temp images"""
158 if not os.path.isdir(self._ConvertedWallpaperLocation):
159 os.mkdir(self._ConvertedWallpaperLocation)
160 for fullpath, filenames, dirnames in os.walk(self._ConvertedWallpaperLocation, topdown=False):
161 for filename in filenames:
162 os.unlink(os.path.join(fullpath, filename))
163 for dirname in dirnames:
164 os.unlink(os.path.join(fullpath, dirname))
165
166 def convert_image_format(self, filename):
167 """Convert the image to a png, and store it in a local place"""
168 self.remove_old_image_cache()
169 output_name = os.path.join(self._ConvertedWallpaperLocation, '%s.png' % time.time())
170 try:
171 return super(OSXChanger, self).convert_image_format(filename, format='PNG', extension='.png')
172 except ImportError:
173 logging.debug('Could not load PIL, going to try just copying the image')
174 import shutil
175 output_name = os.path.join(self._ConvertedWallpaperLocation, os.path.basename(filename))
176 shutil.copyfile(filename, output_name)
177 return output_name, True
178
179 def fix_desktop_plist(self):
180 """Removes the entry in the desktop plist file that specifies the wallpaper for each monitor"""
181 try:
182 import Foundation
183 desktop_plist = Foundation.NSMutableDictionary.dictionaryWithContentsOfFile_(self._DesktopPlistLocation)
184 # Remove all but the 'default' entry
185 for k in desktop_plist['Background'].keys():
186 if k == 'default':
187 continue
188 desktop_plist['Background'].removeObjectForKey_(k)
189 # Store the plist again (Make sure we write it out atomically -- Don't want to break finder)
190 desktop_plist.writeToFile_atomically_(self._DesktopPlistLocation, True)
191 except ImportError:
192 logging.debug('Could not import the Foundation module, you may have problems with dual screens')
193
194 def set_image(self, filename):
195 self.fix_desktop_plist()
196 if self.convert:
197 filename, ret = self.convert_image_format(filename)
198 if not ret:
199 logging.debug("Convert failed")
200 return False
201 cmd = """osascript -e 'tell application "finder" to set desktop picture to posix file "%s"'""" % filename
202 logging.debug(cmd)
203 return not subprocess.getstatusoutput(cmd)[0]
204
205 class WIN32Changer(BaseChanger):
206 name = "Windows"
207 _ConvertedWallpaperLocation = os.path.join(os.environ.get('APPDATA', os.path.expanduser('~')), 'wallchanger')
208
209 def __init__(self, *args, **kwargs):
210 BaseChanger.__init__(self, *args, **kwargs)
211 if not self.convert:
212 logging.warn('Running on windows, but convert is not set')
213
214 def remove_old_image_cache(self):
215 """Cleans up any old temp images"""
216 if not os.path.isdir(self._ConvertedWallpaperLocation):
217 os.mkdir(self._ConvertedWallpaperLocation)
218 for fullpath, filenames, dirnames in os.walk(self._ConvertedWallpaperLocation, topdown=False):
219 for filename in filenames:
220 os.unlink(os.path.join(fullpath, filename))
221 for dirname in dirnames:
222 os.unlink(os.path.join(fullpath, dirname))
223
224 def set_image(self, filename):
225 import ctypes
226 user32 = ctypes.windll.user32
227
228 # Taken from the Platform SDK
229 SPI_SETDESKWALLPAPER = 20
230 SPIF_SENDWININICHANGE = 2
231
232 if self.convert:
233 filename, ret = self.convert_image_format(filename)
234 if not ret:
235 logging.debug("Convert failed")
236 return False
237
238 # Parameters for SystemParametersInfoA are:
239 # (UINT uiAction, UINT uiParam, PVOID pvParam, UINT fWinIni)
240 user32.SystemParametersInfoA(
241 SPI_SETDESKWALLPAPER,
242 0,
243 filename,
244 SPIF_SENDWININICHANGE,
245 )
246 return True
247
248 class Gnome2Changer(BaseChanger):
249 name = "Gnome"
250 def set_image(self, filename):
251 cmd = ['gconftool-2', '--type', 'string', '--set', '/desktop/gnome/background/picture_filename', filename]
252 logging.debug(cmd)
253 return subprocess.call(cmd) == 0
254
255 class Gnome3Changer(BaseChanger):
256 name = "Gnome3"
257 def set_image(self, filename):
258 cmd = ['gsettings', 'set', 'org.gnome.desktop.background', 'picture-uri', 'file://'+filename]
259 logging.debug(cmd)
260 return subprocess.call(cmd) == 0
261
262 class Xfce4Changer(BaseChanger):
263 name = "XFCE4"
264 def set_image(self, filename):
265 for display in get_displays():
266 cmd = [
267 "xfconf-query",
268 "-c", "xfce4-desktop",
269 "-p", "/backdrop/screen0/monitor%s/workspace0/last-image" % display,
270 "-s", filename,
271 ]
272 logging.debug(cmd)
273 if subprocess.call(cmd) != 0:
274 return False
275 return True
276
277 def main(filename):
278 logging.basicConfig(level=logging.DEBUG, format="%(levelname)s: %(message)s")
279 init()
280 set_image(filename)
281
282 if __name__ == "__main__":
283 try:
284 filename = sys.argv[1]
285 except:
286 print("Usage: %s filename" % sys.argv[0], file=sys.stderr)
287 sys.exit(1)
288
289 main(filename)
290
291