# Preferences class for use in TextMate commands/bundles. # # Offers a hash interface for data that is persisted in a YAML file in the user's preference dir. # See http://henrik.nyh.se/2007/06/preferences-class-for-textmate-commands for more information. # # By Henrik Nyh 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 # The rest is unit tests, which you don't have to include in your command or bundle. if __FILE__ == $0 require "test/unit" class TextMatePreferencesTest < Test::Unit::TestCase def assert_simultaneous(one, two) assert_equal one.to_i, two.to_i end def assert_stored(data, method = :assert_equal) Preferences[:test][:key] = data send(method, data, Preferences[:test][:key]) # From cache Preferences[:test].flush_cache! send(method, data, Preferences[:test][:key]) # Loaded from plist end def setup Preferences[:test].clear! end def teardown Preferences[:test].clear! end def test_is_singleton assert_equal Preferences[:test], Preferences[:test] assert_not_equal Preferences[:test], Preferences[:test2] end def test_accepts_filenameable_preference_ids ['a', 'abcd', '1', 'ab34', '_', 'ab34_'].each do |id| assert_nothing_raised(ArgumentError) { Preferences[id] } end end def test_doesnt_accept_weird_preference_ids [nil, '', ' ', :' ', '!'].each do |id| assert_raise(ArgumentError) { Preferences[id] } end end def test_undefined_values_are_nil assert_equal nil, Preferences[:test][:foo] end def test_symbol_and_string_ids_and_keys_are_equivalent Preferences[:test][:foo] = "bar" assert_equal Preferences[:test][:foo], Preferences["test"]["foo"] end def test_can_persist_ints_floats_nil_bools_strings_symbols_nested_arrays_and_hashes data = [-1, 0, 1, 1.5, nil, true, false, [[["a"], "nested"], ["array!"]], {:foo => :bar}] assert_stored data end def test_can_persist_times # We need assert_simultaneous since assert_equal(Time.now, Time.now) is false assert_stored Time.now, :assert_simultaneous end def test_can_delete Preferences[:test][:foo] = "foo" Preferences[:test][:bar] = "bar" assert_equal({:foo => "foo", :bar => "bar"}, Preferences[:test].to_hash) Preferences[:test].delete(:foo) Preferences[:test].flush_cache! assert_equal({:bar => "bar"}, Preferences[:test].to_hash) end end end