A smart wallpaper setter for multi-head displays
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

smartbg 5.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. #!/usr/bin/env python2
  2. # -*- mode: python -*-
  3. # This program is free software. It comes without any warranty, to
  4. # the extent permitted by applicable law. You can redistribute it
  5. # and/or modify it under the terms of the Do What The Fuck You Want
  6. # To Public License, Version 2, as published by Sam Hocevar. See
  7. # http://sam.zoy.org/wtfpl/COPYING for more details.
  8. import glob
  9. import os.path
  10. import optparse
  11. import random
  12. import time
  13. import pygtk
  14. pygtk.require('2.0')
  15. import gtk
  16. import Xlib.Xatom, Xlib.display
  17. import Xlib.X as X
  18. IMG_EXTENSIONS = ("bmp", "jpg", "jpeg", "png")
  19. TRANSITION_FPS = 25
  20. def get_image_path(args):
  21. paths = []
  22. for path in args:
  23. if os.path.isdir(path):
  24. # Add all matching images from this directory
  25. for ext in IMG_EXTENSIONS:
  26. paths.extend(glob.iglob(os.path.join(path, "*.%s" % ext)))
  27. elif os.path.isfile(path):
  28. paths.append(path)
  29. else:
  30. raise Exception("%s is not a file nor a directory" % path)
  31. if len(paths) > 0:
  32. return random.choice(paths)
  33. else:
  34. raise Exception("Missing path (directory or file) on the command line")
  35. def add_image_to_pixbuf(pb, geom, img):
  36. gw, gh = geom.width, geom.height
  37. gx, gy = geom.x, geom.y
  38. pix = gtk.gdk.pixbuf_new_from_file(img)
  39. iw, ih = pix.get_width(), pix.get_height()
  40. print "Setting image for display %d to %s..." % (mon, img)
  41. print " * image size: %dx%d" % (iw, ih)
  42. print " * display geometry: %dx%d+%d+%d" % (gw, gh, gx, gy)
  43. # Scaling needed?
  44. if iw > gw or ih > gh or (iw < gw and ih < gh):
  45. ratio = float(iw)/float(ih)
  46. # Try width first
  47. if gw / ratio <= gh:
  48. print " * scaling (based on height)"
  49. iw = gw
  50. ih = int(gw / ratio)
  51. else:
  52. print " * scaling (based on width)"
  53. ih = gh
  54. iw = int(gh * ratio)
  55. print " * new size: %dx%d" % (iw, ih)
  56. pix = pix.scale_simple(iw, ih, gtk.gdk.INTERP_BILINEAR)
  57. # Draw (centered) on the relevant part of the window
  58. off_x = (gw - iw) / 2
  59. off_y = (gh - ih) / 2
  60. print " * offsets: (%d, %d)" % (off_x, off_y)
  61. pix.copy_area(0, 0, iw, ih, pb, gx + off_x, gy + off_y)
  62. def set_wallpaper(pb):
  63. # Get a pixmap from the pixbuf
  64. (pm, mask) = pb.render_pixmap_and_mask()
  65. # First draw with Gtk (or else things may fail strangely)
  66. win = gtk.gdk.get_default_root_window()
  67. win.set_back_pixmap(pm, False)
  68. win.clear()
  69. win.draw_pixbuf(None, pb, 0, 0, 0, 0, sw, sh)
  70. # Then let the Xlib fun begin!
  71. # This is largely inspired by feh and esetroot
  72. # (http://www.eterm.org/docs/view.php?doc=ref#trans)
  73. # Create a new display,
  74. dpy = Xlib.display.Display()
  75. xscr = dpy.screen()
  76. xwin = xscr.root
  77. # Copy pixmap to this display
  78. pm2 = xwin.create_pixmap(xscr.width_in_pixels, xscr.height_in_pixels, xscr.root_depth)
  79. gc = pm2.create_gc(fill_style=X.FillTiled, tile=pm.xid)
  80. pm2.fill_rectangle(gc, 0, 0, xscr.width_in_pixels, xscr.height_in_pixels)
  81. gc.free()
  82. dpy.sync()
  83. # Check if the properties exist and are equal
  84. prop_root = dpy.intern_atom("_XROOTPMAP_ID", True)
  85. prop_esetroot = dpy.intern_atom("ESETROOT_PMAP_ID", True)
  86. if prop_root != X.NONE and prop_esetroot != X.NONE:
  87. p1 = xwin.get_property(prop_root, X.AnyPropertyType, 0, 1, False)
  88. if p1 is not None and p1.property_type == Xlib.Xatom.PIXMAP:
  89. p2 = xwin.get_property(prop_root, X.AnyPropertyType, 0, 1, False)
  90. if p2 is not None and p2.property_type == Xlib.Xatom.PIXMAP and p1.value == p2.value:
  91. # It's safe: kill the pixmap
  92. xpm = dpy.create_resource_object("pixmap", p1.value[0])
  93. xpm.kill_client()
  94. # Locate the properties, create them if they don't exist
  95. prop_root = dpy.intern_atom("_XROOTPMAP_ID", False)
  96. prop_esetroot = dpy.intern_atom("ESETROOT_PMAP_ID", False)
  97. if prop_root == X.NONE or prop_esetroot == X.NONE:
  98. raise ValueError("prop is X.NONE")
  99. # Set them
  100. xwin.change_property(prop_root, Xlib.Xatom.PIXMAP, 32, [pm2.__resource__()], X.PropModeReplace)
  101. xwin.change_property(prop_esetroot, Xlib.Xatom.PIXMAP, 32, [pm2.__resource__()], X.PropModeReplace)
  102. # Now set the root window background pixmap
  103. xwin.change_attributes(background_pixmap=pm2.__resource__())
  104. xwin.clear_area()
  105. # And we're done!
  106. dpy.set_close_down_mode(X.RetainPermanent)
  107. dpy.sync()
  108. dpy.close()
  109. # Parse command-line
  110. parser = optparse.OptionParser()
  111. parser.add_option("-d", "--duration", dest="duration", type=int, default=0,
  112. help="Set the duration of the transition (in milliseconds)")
  113. (options, args) = parser.parse_args()
  114. # Root window and screen geometry
  115. scr = gtk.gdk.screen_get_default()
  116. win = gtk.gdk.get_default_root_window()
  117. sw, sh = scr.get_width(), scr.get_height()
  118. # Pixbuf for rendering the pictures
  119. wp = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, sw, sh)
  120. wp.fill(0x000000ff)
  121. # Prepare an image for each monitor
  122. for mon in range(scr.get_n_monitors()):
  123. geom = scr.get_monitor_geometry(mon)
  124. img = get_image_path(args)
  125. add_image_to_pixbuf(wp, geom, img)
  126. if options.duration > 0:
  127. # Prepare the transition
  128. step_duration = 1000. / TRANSITION_FPS
  129. steps = int((options.duration / step_duration) + .5)
  130. cm = win.get_colormap()
  131. alpha = 0.
  132. d = 0.
  133. for step in xrange(steps):
  134. if d > 0.:
  135. time.sleep(d / 1000.)
  136. t1 = time.time()
  137. obj = (step + 1.) / steps
  138. beta = (obj - alpha) / (1. - alpha)
  139. ctx = win.cairo_create()
  140. ctx.set_source_pixbuf(wp, 0, 0)
  141. ctx.paint_with_alpha(beta)
  142. alpha = beta
  143. t2 = time.time()
  144. d = step_duration - ((t2 - t1) * 1000.)
  145. # Now render the wallpaper for good (even if there was a transition)
  146. set_wallpaper(wp)