Preferences class for TextMate commands

Written June 28, 2007 at 00:34 CEST. Tagged Ruby, OS X and TextMate.

In TextMate commands, you sometimes need to to persist information. Perhaps you want to remember what box the user checked, or their last input in some dialog.

When I needed persistence in my Greasemonkey bundle a while back, I wrapped the functionality in a Preferences class to keep things tidy. When I needed it again today, I tidied up that class further and moved it to a YAML storage which made for less and nicer-looking code.

You can use it like so:

# require or paste the Preferences class here

# Store some values

prefs = Preferences[:foo_bundle]
prefs[:name] = "John Doe"
prefs[:age] = 42
prefs.merge!(:likes_pizza => true, :likes_broccoli => false)

# A reboot later…

prefs = Preferences[:foo_bundle]
puts "Your name is #{prefs[:name]} and you are #{prefs[:age]} years old."
puts "I know all this:"
p prefs.to_hash

# Forget age
prefs.delete(:age)

# Forget everything
prefs.clear!

String and symbol keys are interchangeable, so you could use Preferences["foo_bundle"]["name"] if you prefer, or even mix and match.

Download with unit tests and all, or see below for the meaty bits:

# By Henrik Nyh <http://henrik.nyh.se> 2007-06-27
# Free to modify and redistribute with credit.

class Preferences
  %w{yaml fileutils}.each { |lib| require lib }
  
  # Each preference id should be a singleton

  @@instances = {}

  class << self; private :new; end

  def self.[](id)
    raise(ArgumentError, "Preference id must match /\A[a-zA-Z0-9_]+\Z/.") unless id.to_s =~ /\A\w+\Z/
    @@instances[id.to_sym] ||= new(id)
  end
  
  def initialize(id)
    @id = id.to_s
  end
  
  # Singleton code ends
    
  def [](key)
    value = to_hash[key.to_sym]
  end
  
  def []=(key, value)
    merge!({key => value})
    value
  end
  
  def merge!(new_hash)
    symbolized_keys = new_hash.inject({}) { |h, (k, v)| h[k.to_sym] = v; h }
    @hash = to_hash.merge(symbolized_keys)
    persist!
  end
  
  def delete(key)
    key = key.to_sym
    value = self[key]
    @hash.delete(key)
    persist!
    value
  end
  
  def clear!
    File.delete(path) if File.exist?(path)
    flush_cache!
  end
    
  def flush_cache!
    @hash = nil
  end
  
  def to_hash
    @hash ||= YAML.load_file(path) rescue {}
  end
  
private

  def path
    "#{ENV['HOME']}/Library/Preferences/com.macromates.textmate.#{@id}.yaml"
  end
  
  def persist!
    File.open(path, "w") { |out| YAML.dump(@hash, out) }
    @hash
  end
  
end