Desktop background on OS X 10.9 Mavericks

Published
2 December 2013
Tagged

I upgraded to OS X 10.9 a week or so ago, along with everyone else in the Mac nerd world. And like everyone else in the Mac nerd world, something of mine broke. In my case, it was a small script that would alter com.apple.desktop.plist to daily change my desktop.

Essentially, you have no way in OS X of syncing desktop backgrounds between spaces. You can set desktops so that all your spaces have the same desktop, but if you want to cycle them randomly, the first time the OS changes them, they'll be all different once again. The problem is not a huge one, but it fell into a particular category of being big enough to annoy me, and small enough that I could write a script to fix it.

Under OS X Lion (and before) these preferences were stored in ~/Library/Preferences/com.apple.desktop.plist, and my original script would open this up and edit values for each space. However, as soon as I moved to Mavericks, this broke. The script would continue to edit the plist file, but the desktop wouldn't change in response.

The internet was of no help - I imagine this problem is pretty niche - so I ended up poking around in the guts of ~/Library/ to work out where OS X now stored desktop preferences. After several sessions of searching, I came across a promising file: ~/Application Support/Dock/desktoppicture.db. This is an SQLite database file, and poking around in its innards, it definitely seems to be linked to desktop background preferences:

> sqlite3 desktoppicture.db 
SQLite version 3.7.13 2012-07-17 17:46:21
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> .tables
data         displays     pictures     preferences  prefs        spaces     
sqlite> SELECT * FROM data;
~/Archive/Wallpapers/SimpleD
~/Archive/Wallpapers/SimpleD/code.png
~/Archive/Wallpapers/Merek
~/Archive/Wallpapers/Merek/Purple_Rain-Retina.jpg
~/Archive/Wallpapers/SimpleD/old-fashioned.png

At first, some of the tables look a bit cryptic:

sqlite> .header ON
sqlite> SELECT * FROM preferences;
key|data_id|picture_id
1|5|8
10|4|8
10|4|9
1|5|9
1|5|10
10|4|10
10|4|11
1|5|11
1|7|2
10|6|2
10|6|3
1|7|3
1|7|14
10|6|14
10|6|15
1|7|15
1|5|12
10|4|12
10|4|13
1|5|13
10|6|4
1|7|4
1|11|5
10|4|5
10|4|6
1|11|6
10|4|7
1|11|7
10|4|1
1|11|1

However, after a bit of investigation I pieced together some structure to them:

desktoppicture.db table layout. Arrows indicate references to other tables. I never took a course in proper UML.

desktoppicture.db table layout. Arrows indicate references to other tables. I never took a course in proper UML.

This is how the tables relate:

  • data stores a series of files and folders, which represent either desktop backgrounds or folders of images.
  • displays is a list of the computer displays, each of which is given a UUID to differentiate it from other displays.
  • spaces is a list of spaces currently operating on the computer. Again, each gets a UUID except for one: the primary space has no UUID.
  • pictures is a somewhat badly-named table. Each entry in pictures links a display and a space, although both references can also be null. I'm not sure why this table exists, since I'd expect you could more easily link each space to its display. Regardless, this table has a list of space-display relationships.
  • preferences is a list of relationships between entries in the data table (i.e. folder and file paths) and entries in the pictures table (i.e. space/display combinations). Each entry also has an integer value key. It appears that if key is 1, the entry indicates a background image, and if the key it 10, the entry indicates a folder. Presumably this is for OS-mandated background-changing algorithms.
  • prefs is a somewhat unrelated table, in which I assume the system stores information about whether to change desktop backgrounds regularly.

While this is educational, the main thing we learn is that we hardly have to touch any of the database. All we need to do is extract the filenames from the data table, use them to pick new file names, and then update the table. Here's the full ruby code to do this:

#!/usr/bin/env ruby

require "sqlite3"

DB_LOC = File.join(ENV["HOME"], "Library/Application Support/Dock/desktoppicture.db")
DB = SQLite3::Database.new(DB_LOC)

DB.execute("SELECT ROWID,value FROM data").each do |(rowid,path)|
  path = File.expand_path(path)
  next if File.directory?(path)

  image_dir = File.dirname(path)
  new_image = Dir[File.join(image_dir,"*")].sort_by{ rand }[0]
  new_image.sub!(ENV["HOME"],"~")

  DB.execute("UPDATE data SET value=? WHERE ROWID=?",[new_image,rowid])  
end

`killall Dock`

We need to use File::expand_path because the database quite happily encodes your home directory as ~, which ruby doesn't like. Similarly, we sub the home directory out at the end to put it in a form the system will like. We could probably get away with leaving the full path in there, but better safe than sorry. Finally, we need to run killall to restart the dock: it looks like otherwise the deskop won't change background.

So there we go. It took a while to work out what was wrong, but Apple's replacement desktop background file is surprisingly easy to edit.