Preferences class for TextMate commands

Written . Tagged OS X, Ruby, 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# 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