]> git.phdru.name Git - xsetbg.git/blob - xsetbg.py
Parse timedelta in ymdThms format.
[xsetbg.git] / xsetbg.py
1 #! /usr/bin/env python
2 """Set a random background image (XWin)
3
4 Select a random image from a (list of) directory(s)
5 and set it as the desktop wallpaper (display it in the root window)
6 using xli program.
7
8 """
9
10 __author__ = "Oleg Broytman <phd@phdru.name>"
11 __copyright__ = "Copyright (C) 2000-2012 PhiloSoft Design"
12 __license__ = "GNU GPL"
13
14 __all__ = ['host', 'port', 'change']
15
16
17 import sys, os
18 from ConfigParser import SafeConfigParser
19
20 import anydbm
21 from datetime import timedelta
22 import random
23 import re
24 import shelve
25 import subprocess
26 from time import time
27 from fcntl import flock, LOCK_EX, LOCK_UN, LOCK_NB
28
29 def error(error_str, error_code=1):
30    sys.stderr.write("%s: Error: %s\n" % (sys.argv[0], error_str))
31    sys.exit(error_code)
32
33
34 xsetbg_dir = os.path.dirname(os.path.abspath(__file__))
35 os.chdir(xsetbg_dir)
36
37 config = SafeConfigParser()
38 config.read("xsetbg.conf")
39
40 if config.has_option("images", "directory") or \
41    config.has_option("images", "directory0") or \
42    config.has_option("images", "directory1"):
43    image_dirs = []
44    if config.has_option("images", "directory"):
45       image_dirs.append(config.get("images", "directory"))
46    if config.has_option("images", "directory0"):
47       image_dirs.append(config.get("images", "directory0"))
48    if config.has_option("images", "directory1"):
49       image_dirs.append(config.get("images", "directory1"))
50    i = 2
51    while True:
52       option = "directory%d" % i
53       if config.has_option("images", option):
54          image_dirs.append(config.get("images", option))
55          i += 1
56       else:
57          break
58 else:
59    image_dirs = ["images"]
60
61 image_dirs = [os.path.join(xsetbg_dir,
62    os.path.expandvars(os.path.expanduser(dirname)))
63       for dirname in image_dirs
64 ]
65
66 # minimum time in seconds between background image changes
67 if config.has_option("xsetbg", "min_pause"):
68    min_pause = config.getint("xsetbg", "min_pause")
69 else:
70    min_pause = 60
71
72 borders = config.get("xsetbg", "borders").split(',')
73 if config.has_option("xsetbg", "borders"):
74    borders = [border.strip() for border in config.get("xsetbg", "borders").split(',')]
75 else:
76    borders = ["darkcyan", "steelblue", "midnightblue"]
77
78 # minimum time in seconds between occurences of the same image
79 if config.has_option("xsetbg", "min_delay"):
80    min_delay = config.get("xsetbg", "min_delay")
81
82    # Borrowed from http://stackoverflow.com/a/2765366
83    td_re  = re.compile('(?:(?P<years>\d+)y)?(?:(?P<months>\d+)m)?(?:(?P<days>\d+)d)?(?:T(?:(?P<hours>\d+)h)?(?:(?P<minutes>\d+)m)?(?:(?P<seconds>\d+)s)?)?')
84    td_dict = td_re.match(min_delay).groupdict(0)
85    delta = timedelta(days=int(td_dict['days']) + (int(td_dict['months']) * 30) + (int(td_dict['years']) * 365),
86                      hours=int(td_dict['hours']),
87                      minutes=int(td_dict['minutes']),
88                      seconds=int(td_dict['seconds']))
89
90    if delta:
91       min_delay = delta.days * 24*3600 + delta.seconds
92    else:
93       min_delay = int(min_delay)
94
95 else:
96    min_delay = 3600*24 # 24 hours
97
98 # httpd settings
99 if config.has_option("httpd", "host"):
100    host = config.get("httpd", "host")
101 else:
102    host = 'localhost'
103
104 if config.has_option("httpd", "port"):
105    port = config.getint("httpd", "port")
106 else:
107    error("Config must specify a port to listen. Abort.")
108
109 del config
110
111
112 os.umask(0066) # octal; -rw-------; make the global persistent dictionary
113                # readable only by the user
114 global_db_name = "xsetbg.db"
115
116
117 # DB keys
118 timestamp_key = "timestamp"
119 filename_key = "filename"
120 old_filename_key = "old_filename"
121
122
123 # Create the database if it is not exists yet.
124
125 try:
126    global_db = shelve.open(global_db_name, flag='c')
127 except anydbm.error, msg:
128    if str(msg) == "db type could not be determined":
129       os.remove(global_db_name)
130       global_db = shelve.open(global_db_name, flag='c')
131
132 # Remove old filenames
133 old_time = time() - min_delay
134
135 to_delete = [timestamp_key]
136 for key in global_db.keys():
137    if key.startswith('/') and global_db[key] < old_time:
138       to_delete.append(key)
139
140 for key in to_delete:
141    try:
142       del global_db[key]
143    except KeyError:
144       pass
145
146 global_db.close() # Close DB in the parent process
147
148
149 images = []
150
151 for image_dir in image_dirs:
152    # List images in all subdirectories
153    for dirpath, dirs, files in os.walk(image_dir):
154       images.extend([os.path.join(dirpath, file) for file in files])
155
156 if not images:
157    error("No images found. Abort.")
158
159
160 def change(force=False):
161    # Use the program's file as the lock file:
162    # lock it to prevent two processes run in parallel.
163    lock_file = open("xsetbg.py", 'r')
164    try:
165       flock(lock_file, LOCK_EX|LOCK_NB)
166    except IOError: # already locked
167       lock_file.close()
168       return
169
170
171    global_db = None
172    try:
173       # Reopen the global persistent dictionary
174       global_db = shelve.open(global_db_name, 'w')
175
176       timestamp = global_db.get(timestamp_key)
177       current_time = time()
178
179       if not force and timestamp is not None and \
180          current_time - timestamp < min_pause: # Too early to change background
181             return
182
183       # Save current time
184       global_db[timestamp_key] = current_time
185
186       # Select a random image and check if we've seen it recently;
187       # loop until we can find a new image (never seen before) or old enough.
188       for i in xrange(len(images)): # ensure the loop is not infinite
189          image_name = random.choice(images)
190          if global_db.has_key(image_name):
191             image_time = global_db[image_name]
192             if current_time - image_time > min_delay:
193                break
194          else:
195             break
196       global_db[image_name] = current_time
197
198       # Save filename
199       if global_db.has_key(filename_key):
200          global_db[old_filename_key] = global_db[filename_key]
201       global_db[filename_key] = image_name
202
203       program_options = ["xli", "-onroot", "-quiet"] + \
204          ["-center", "-border", random.choice(borders), "-zoom", "auto",
205             image_name]
206
207       rc = subprocess.call(program_options)
208       if rc:
209          error("cannot execute xli!")
210
211    finally:
212       # Unlock and close the lock file
213       flock(lock_file, LOCK_UN)
214       lock_file.close()
215       # Flush and close the global persistent dictionary
216       if global_db: global_db.close()